Merge pull request #23047 from civicrm/5.48
[civicrm-core.git] / Civi / Api4 / Utils / ReflectionUtils.php
1 <?php
2 /*
3 +--------------------------------------------------------------------+
4 | Copyright CiviCRM LLC. All rights reserved. |
5 | |
6 | This work is published under the GNU AGPLv3 license with some |
7 | permitted exceptions and without any warranty. For full license |
8 | and copyright information, see https://civicrm.org/licensing |
9 +--------------------------------------------------------------------+
10 */
11
12 namespace Civi\Api4\Utils;
13
14 /**
15 * Just another place to put static functions...
16 */
17 class ReflectionUtils {
18
19 /**
20 * @param \Reflector|\ReflectionClass $reflection
21 * @param string $type
22 * If we are not reflecting the class itself, specify "Method", "Property", etc.
23 * @param array $vars
24 * Variable substitutions to perform in the docblock
25 * @return array
26 */
27 public static function getCodeDocs($reflection, $type = NULL, $vars = []) {
28 $comment = $reflection->getDocComment();
29 foreach ($vars as $key => $val) {
30 $comment = str_replace('$' . strtoupper(\CRM_Utils_String::pluralize($key)), \CRM_Utils_String::pluralize($val), $comment);
31 $comment = str_replace('$' . strtoupper($key), $val, $comment);
32 }
33 $docs = self::parseDocBlock($comment);
34
35 // Recurse into parent functions
36 if (isset($docs['inheritDoc']) || isset($docs['inheritdoc'])) {
37 unset($docs['inheritDoc'], $docs['inheritdoc']);
38 $newReflection = NULL;
39 try {
40 if ($type) {
41 $name = $reflection->getName();
42 $reflectionClass = $reflection->getDeclaringClass()->getParentClass();
43 if ($reflectionClass) {
44 $getItem = "get$type";
45 $newReflection = $reflectionClass->$getItem($name);
46 }
47 }
48 else {
49 $newReflection = $reflection->getParentClass();
50 }
51 }
52 catch (\ReflectionException $e) {
53 }
54 if ($newReflection) {
55 // Mix in
56 $additionalDocs = self::getCodeDocs($newReflection, $type, $vars);
57 if (!empty($docs['comment']) && !empty($additionalDocs['comment'])) {
58 $docs['comment'] .= "\n\n" . $additionalDocs['comment'];
59 }
60 $docs += $additionalDocs;
61 }
62 }
63 return $docs;
64 }
65
66 /**
67 * @param string $comment
68 * @return array
69 */
70 public static function parseDocBlock($comment) {
71 $info = [];
72 $param = NULL;
73 foreach (preg_split("/((\r?\n)|(\r\n?))/", $comment) as $num => $line) {
74 if (!$num || strpos($line, '*/') !== FALSE) {
75 continue;
76 }
77 $line = ltrim(trim($line), '*');
78 if (strlen($line) && $line[0] === ' ') {
79 $line = substr($line, 1);
80 }
81 if (strpos(ltrim($line), '@') === 0) {
82 $words = explode(' ', ltrim($line, ' @'));
83 $key = array_shift($words);
84 $param = NULL;
85 if ($key == 'var') {
86 $info['type'] = explode('|', $words[0]);
87 }
88 elseif ($key == 'return') {
89 $info['return'] = explode('|', $words[0]);
90 }
91 elseif ($key == 'options' || $key == 'ui_join_filters' || $key == 'groupWeightsBy') {
92 $val = str_replace(', ', ',', implode(' ', $words));
93 $info[$key] = explode(',', $val);
94 }
95 elseif ($key == 'throws' || $key == 'see') {
96 $info[$key][] = implode(' ', $words);
97 }
98 elseif ($key == 'param' && $words) {
99 $type = $words[0][0] !== '$' ? explode('|', array_shift($words)) : NULL;
100 $param = rtrim(array_shift($words), '-:()/');
101 $info['params'][$param] = [
102 'type' => $type,
103 'description' => $words ? ltrim(implode(' ', $words), '-: ') : '',
104 'comment' => '',
105 ];
106 }
107 else {
108 // Unrecognized annotation, but we'll duly add it to the info array
109 $val = implode(' ', $words);
110 $info[$key] = strlen($val) ? $val : TRUE;
111 }
112 }
113 elseif ($param) {
114 $info['params'][$param]['comment'] .= $line . "\n";
115 }
116 elseif ($num == 1) {
117 $info['description'] = ucfirst($line);
118 }
119 elseif (!$line) {
120 if (isset($info['comment'])) {
121 $info['comment'] .= "\n";
122 }
123 else {
124 $info['comment'] = NULL;
125 }
126 }
127 // For multi-line description.
128 elseif (count($info) === 1 && isset($info['description']) && substr($info['description'], -1) !== '.') {
129 $info['description'] .= ' ' . $line;
130 }
131 else {
132 $info['comment'] = isset($info['comment']) ? "{$info['comment']}\n$line" : $line;
133 }
134 }
135 if (isset($info['comment'])) {
136 $info['comment'] = rtrim($info['comment']);
137 }
138 return $info;
139 }
140
141 /**
142 * List all traits used by a class and its parents.
143 *
144 * @param object|string $class
145 * @return array
146 */
147 public static function getTraits($class) {
148 $traits = [];
149 // Get traits of this class + parent classes
150 do {
151 $traits = array_merge(class_uses($class), $traits);
152 } while ($class = get_parent_class($class));
153 // Get traits of traits
154 foreach ($traits as $trait => $same) {
155 $traits = array_merge(class_uses($trait), $traits);
156 }
157 return $traits;
158 }
159
160 /**
161 * Get a list of standard properties which can be written+read by outside callers.
162 *
163 * @param string $class
164 */
165 public static function findStandardProperties($class): iterable {
166 try {
167 /** @var \ReflectionClass $clazz */
168 $clazz = new \ReflectionClass($class);
169
170 yield from [];
171 foreach ($clazz->getProperties(\ReflectionProperty::IS_PROTECTED | \ReflectionProperty::IS_PUBLIC) as $property) {
172 if (!$property->isStatic() && $property->getName()[0] !== '_') {
173 yield $property;
174 }
175 }
176 }
177 catch (\ReflectionException $e) {
178 throw new \RuntimeException(sprintf("Cannot inspect class %s.", $class));
179 }
180 }
181
182 /**
183 * Check if a class method is deprecated
184 *
185 * @param string $className
186 * @param string $methodName
187 * @return bool
188 * @throws \ReflectionException
189 */
190 public static function isMethodDeprecated(string $className, string $methodName): bool {
191 $reflection = new \ReflectionClass($className);
192 $docBlock = $reflection->getMethod($methodName)->getDocComment();
193 return strpos($docBlock, "@deprecated") !== FALSE;
194 }
195
196 /**
197 * Find any methods in this class which match the given prefix.
198 *
199 * @param string $class
200 * @param string $prefix
201 */
202 public static function findMethodHelpers($class, string $prefix): iterable {
203 try {
204 /** @var \ReflectionClass $clazz */
205 $clazz = new \ReflectionClass($class);
206
207 yield from [];
208 foreach ($clazz->getMethods(\ReflectionMethod::IS_PUBLIC | \ReflectionMethod::IS_PROTECTED) as $m) {
209 if (\CRM_Utils_String::startsWith($m->getName(), $prefix)) {
210 yield $m;
211 }
212 }
213 }
214 catch (\ReflectionException $e) {
215 throw new \RuntimeException(sprintf("Cannot inspect class %s.", $class));
216 }
217 }
218
219 }