1 | <?php |
||||||
2 | /** |
||||||
3 | * Twigfield for Craft CMS |
||||||
4 | * |
||||||
5 | * Provides a twig editor field with Twig & Craft API autocomplete |
||||||
6 | * |
||||||
7 | * @link https://nystudio107.com |
||||||
0 ignored issues
–
show
Coding Style
introduced
by
Loading history...
|
|||||||
8 | * @copyright Copyright (c) 2022 nystudio107 |
||||||
0 ignored issues
–
show
|
|||||||
9 | */ |
||||||
0 ignored issues
–
show
|
|||||||
10 | |||||||
11 | namespace nystudio107\twigfield\base; |
||||||
12 | |||||||
13 | use craft\base\Element; |
||||||
14 | use nystudio107\twigfield\models\CompleteItem; |
||||||
15 | use nystudio107\twigfield\types\CompleteItemKind; |
||||||
16 | use phpDocumentor\Reflection\DocBlockFactory; |
||||||
17 | use ReflectionClass; |
||||||
18 | use ReflectionException; |
||||||
19 | use ReflectionMethod; |
||||||
20 | use ReflectionUnionType; |
||||||
21 | use yii\base\Behavior; |
||||||
22 | use yii\base\InvalidConfigException; |
||||||
23 | use yii\di\ServiceLocator; |
||||||
24 | |||||||
25 | /** |
||||||
0 ignored issues
–
show
|
|||||||
26 | * @author nystudio107 |
||||||
0 ignored issues
–
show
Content of the @author tag must be in the form "Display Name <[email protected]>"
Loading history...
|
|||||||
27 | * @package twigfield |
||||||
0 ignored issues
–
show
|
|||||||
28 | * @since 1.0.12 |
||||||
0 ignored issues
–
show
|
|||||||
29 | */ |
||||||
0 ignored issues
–
show
|
|||||||
30 | abstract class ObjectParserAutocomplete extends Autocomplete implements ObjectParserInterface |
||||||
31 | { |
||||||
32 | // Constants |
||||||
33 | // ========================================================================= |
||||||
34 | |||||||
35 | const EXCLUDED_PROPERTY_NAMES = [ |
||||||
36 | 'controller', |
||||||
37 | 'Controller', |
||||||
38 | 'CraftEdition', |
||||||
39 | 'CraftSolo', |
||||||
40 | 'CraftPro', |
||||||
41 | ]; |
||||||
42 | const EXCLUDED_BEHAVIOR_NAMES = [ |
||||||
43 | 'fieldHandles', |
||||||
44 | 'hasMethods', |
||||||
45 | 'owner', |
||||||
46 | ]; |
||||||
47 | const EXCLUDED_PROPERTY_REGEXES = [ |
||||||
48 | '^_', |
||||||
49 | ]; |
||||||
50 | const EXCLUDED_METHOD_REGEXES = [ |
||||||
51 | '^_', |
||||||
52 | ]; |
||||||
53 | const RECURSION_DEPTH_LIMIT = 10; |
||||||
54 | |||||||
55 | // Public Properties |
||||||
56 | // ========================================================================= |
||||||
57 | |||||||
58 | /** |
||||||
0 ignored issues
–
show
|
|||||||
59 | * @var bool If the class itself should be parsed for complete items |
||||||
60 | */ |
||||||
61 | public $parseClass = true; |
||||||
62 | |||||||
63 | /** |
||||||
0 ignored issues
–
show
|
|||||||
64 | * @var bool If any ServiceLocator components should be parsed for complete items |
||||||
65 | */ |
||||||
66 | public $parseComponents = true; |
||||||
67 | |||||||
68 | /** |
||||||
0 ignored issues
–
show
|
|||||||
69 | * @var bool If the class properties should be parsed for complete items |
||||||
70 | */ |
||||||
71 | public $parseProperties = true; |
||||||
72 | |||||||
73 | /** |
||||||
0 ignored issues
–
show
|
|||||||
74 | * @var bool If the class methods should be parsed for complete items |
||||||
75 | */ |
||||||
76 | public $parseMethods = true; |
||||||
77 | |||||||
78 | /** |
||||||
0 ignored issues
–
show
|
|||||||
79 | * @var bool If the class behaviors should be parsed for complete items |
||||||
80 | */ |
||||||
81 | public $parseBehaviors = true; |
||||||
82 | |||||||
83 | /** |
||||||
0 ignored issues
–
show
|
|||||||
84 | * @var string Prefix for custom (behavior) properties, for the complete items sort |
||||||
85 | */ |
||||||
86 | public $customPropertySortPrefix = '~'; |
||||||
87 | |||||||
88 | /** |
||||||
0 ignored issues
–
show
|
|||||||
89 | * @var string Prefix for properties, for the complete items sort |
||||||
90 | */ |
||||||
91 | public $propertySortPrefix = '~~'; |
||||||
92 | |||||||
93 | /** |
||||||
0 ignored issues
–
show
|
|||||||
94 | * @var string Prefix for methods, for the complete items sort |
||||||
95 | */ |
||||||
96 | public $methodSortPrefix = '~~~'; |
||||||
97 | |||||||
98 | // Public Methods |
||||||
99 | // ========================================================================= |
||||||
100 | |||||||
101 | /** |
||||||
0 ignored issues
–
show
|
|||||||
102 | * @inerhitdoc |
||||||
103 | */ |
||||||
0 ignored issues
–
show
|
|||||||
104 | public function parseObject(string $name, $object, int $recursionDepth, string $path = ''): void |
||||||
105 | { |
||||||
106 | // Only recurse `RECURSION_DEPTH_LIMIT` deep |
||||||
107 | if ($recursionDepth > self::RECURSION_DEPTH_LIMIT) { |
||||||
108 | return; |
||||||
109 | } |
||||||
110 | $recursionDepth++; |
||||||
111 | // Create the docblock factory |
||||||
112 | $factory = DocBlockFactory::createInstance(); |
||||||
113 | |||||||
114 | $path = trim(implode('.', [$path, $name]), '.'); |
||||||
115 | // The class itself |
||||||
116 | if ($this->parseClass) { |
||||||
117 | $this->getClassCompletion($object, $factory, $name, $path); |
||||||
118 | } |
||||||
119 | // ServiceLocator Components |
||||||
120 | if ($this->parseComponents) { |
||||||
121 | $this->getComponentCompletion($object, $recursionDepth, $path); |
||||||
122 | } |
||||||
123 | // Class properties |
||||||
124 | if ($this->parseProperties) { |
||||||
125 | $this->getPropertyCompletion($object, $factory, $recursionDepth, $path); |
||||||
126 | } |
||||||
127 | // Class methods |
||||||
128 | if ($this->parseMethods) { |
||||||
129 | $this->getMethodCompletion($object, $factory, $path); |
||||||
130 | } |
||||||
131 | // Behavior properties |
||||||
132 | if ($this->parseBehaviors) { |
||||||
133 | $this->getBehaviorCompletion($object, $factory, $recursionDepth, $path); |
||||||
134 | } |
||||||
135 | } |
||||||
136 | |||||||
137 | // Protected Methods |
||||||
138 | // ========================================================================= |
||||||
139 | |||||||
140 | /** |
||||||
0 ignored issues
–
show
|
|||||||
141 | * @param $object |
||||||
0 ignored issues
–
show
|
|||||||
142 | * @param DocBlockFactory $factory |
||||||
0 ignored issues
–
show
|
|||||||
143 | * @param string $name |
||||||
0 ignored issues
–
show
|
|||||||
144 | * @param $path |
||||||
0 ignored issues
–
show
|
|||||||
145 | */ |
||||||
0 ignored issues
–
show
|
|||||||
146 | protected function getClassCompletion($object, DocBlockFactory $factory, string $name, $path): void |
||||||
147 | { |
||||||
148 | try { |
||||||
149 | $reflectionClass = new ReflectionClass($object); |
||||||
150 | } catch (ReflectionException $e) { |
||||||
151 | return; |
||||||
152 | } |
||||||
153 | // Information on the class itself |
||||||
154 | $className = $reflectionClass->getName(); |
||||||
155 | $docs = $this->getDocs($reflectionClass, $factory); |
||||||
156 | CompleteItem::create() |
||||||
157 | ->detail((string)$className) |
||||||
158 | ->documentation((string)$docs) |
||||||
159 | ->kind(CompleteItemKind::ClassKind) |
||||||
160 | ->label((string)$name) |
||||||
161 | ->insertText((string)$name) |
||||||
162 | ->add($this, $path); |
||||||
163 | } |
||||||
164 | |||||||
165 | /** |
||||||
0 ignored issues
–
show
|
|||||||
166 | * @param $object |
||||||
0 ignored issues
–
show
|
|||||||
167 | * @param $recursionDepth |
||||||
0 ignored issues
–
show
|
|||||||
168 | * @param $path |
||||||
0 ignored issues
–
show
|
|||||||
169 | */ |
||||||
0 ignored issues
–
show
|
|||||||
170 | protected function getComponentCompletion($object, $recursionDepth, $path): void |
||||||
171 | { |
||||||
172 | if ($object instanceof ServiceLocator) { |
||||||
173 | foreach ($object->getComponents() as $key => $value) { |
||||||
174 | $componentObject = null; |
||||||
0 ignored issues
–
show
|
|||||||
175 | try { |
||||||
176 | $componentObject = $object->get($key); |
||||||
177 | } catch (InvalidConfigException $e) { |
||||||
178 | // That's okay |
||||||
179 | } |
||||||
180 | if ($componentObject) { |
||||||
181 | $this->parseObject($key, $componentObject, $recursionDepth, $path); |
||||||
182 | } |
||||||
183 | } |
||||||
184 | } |
||||||
185 | } |
||||||
186 | |||||||
187 | /** |
||||||
0 ignored issues
–
show
|
|||||||
188 | * @param $object |
||||||
0 ignored issues
–
show
|
|||||||
189 | * @param DocBlockFactory $factory |
||||||
0 ignored issues
–
show
|
|||||||
190 | * @param $recursionDepth |
||||||
0 ignored issues
–
show
|
|||||||
191 | * @param string $path |
||||||
0 ignored issues
–
show
|
|||||||
192 | */ |
||||||
0 ignored issues
–
show
|
|||||||
193 | protected function getPropertyCompletion($object, DocBlockFactory $factory, $recursionDepth, string $path): void |
||||||
194 | { |
||||||
195 | try { |
||||||
196 | $reflectionClass = new ReflectionClass($object); |
||||||
197 | } catch (ReflectionException $e) { |
||||||
198 | return; |
||||||
199 | } |
||||||
200 | $reflectionProperties = $reflectionClass->getProperties(); |
||||||
201 | $customField = false; |
||||||
202 | if ($object instanceof Behavior) { |
||||||
203 | $customField = true; |
||||||
204 | } |
||||||
205 | $sortPrefix = $customField ? $this->customPropertySortPrefix : $this->propertySortPrefix; |
||||||
206 | foreach ($reflectionProperties as $reflectionProperty) { |
||||||
207 | $propertyName = $reflectionProperty->getName(); |
||||||
208 | // Exclude some properties |
||||||
209 | $propertyAllowed = true; |
||||||
210 | foreach (self::EXCLUDED_PROPERTY_REGEXES as $excludePattern) { |
||||||
211 | $pattern = '`' . $excludePattern . '`i'; |
||||||
212 | if (preg_match($pattern, $propertyName) === 1) { |
||||||
213 | $propertyAllowed = false; |
||||||
214 | } |
||||||
215 | } |
||||||
216 | if (in_array($propertyName, self::EXCLUDED_PROPERTY_NAMES, true)) { |
||||||
217 | $propertyAllowed = false; |
||||||
218 | } |
||||||
219 | if ($customField && in_array($propertyName, self::EXCLUDED_BEHAVIOR_NAMES, true)) { |
||||||
220 | $propertyAllowed = false; |
||||||
221 | } |
||||||
222 | // Process the property |
||||||
223 | if ($propertyAllowed && $reflectionProperty->isPublic()) { |
||||||
224 | $detail = "Property"; |
||||||
225 | $docblock = null; |
||||||
226 | $docs = $reflectionProperty->getDocComment(); |
||||||
227 | if ($docs) { |
||||||
228 | $docblock = $factory->create($docs); |
||||||
229 | $docs = ''; |
||||||
230 | $summary = $docblock->getSummary(); |
||||||
231 | if (!empty($summary)) { |
||||||
232 | $docs = $summary; |
||||||
233 | } |
||||||
234 | $description = $docblock->getDescription()->render(); |
||||||
235 | if (!empty($description)) { |
||||||
236 | $docs = $description; |
||||||
237 | } |
||||||
238 | } |
||||||
239 | // Figure out the type |
||||||
240 | if ($docblock) { |
||||||
241 | $tag = $docblock->getTagsByName('var'); |
||||||
242 | if ($tag && isset($tag[0])) { |
||||||
243 | $docs = $tag[0]; |
||||||
244 | } |
||||||
245 | } |
||||||
246 | if (preg_match('/@var\s+([^\s]+)/', $docs, $matches)) { |
||||||
247 | list(, $type) = $matches; |
||||||
248 | $detail = $type; |
||||||
249 | } |
||||||
250 | if ($detail === "Property") { |
||||||
251 | if ((PHP_MAJOR_VERSION >= 7 && PHP_MINOR_VERSION >= 4) || (PHP_MAJOR_VERSION >= 8)) { |
||||||
252 | if ($reflectionProperty->hasType()) { |
||||||
253 | $reflectionType = $reflectionProperty->getType(); |
||||||
254 | if ($reflectionType instanceof ReflectionNamedType) { |
||||||
0 ignored issues
–
show
|
|||||||
255 | $type = $reflectionType->getName(); |
||||||
0 ignored issues
–
show
The method
getName() does not exist on ReflectionType . It seems like you code against a sub-type of ReflectionType such as ReflectionNamedType .
(
Ignorable by Annotation
)
If this is a false-positive, you can also ignore this issue in your code via the
Loading history...
|
|||||||
256 | $detail = $type; |
||||||
257 | } |
||||||
258 | } |
||||||
259 | if ((PHP_MAJOR_VERSION >= 8) && $reflectionProperty->hasDefaultValue()) { |
||||||
260 | $value = $reflectionProperty->getDefaultValue(); |
||||||
261 | if (is_array($value)) { |
||||||
262 | $value = json_encode($value); |
||||||
263 | } |
||||||
264 | if (!empty($value)) { |
||||||
265 | $detail = (string)$value; |
||||||
266 | } |
||||||
267 | } |
||||||
268 | } |
||||||
269 | } |
||||||
270 | $thisPath = trim(implode('.', [$path, $propertyName]), '.'); |
||||||
271 | $label = $propertyName; |
||||||
272 | CompleteItem::create() |
||||||
273 | ->detail((string)$detail) |
||||||
274 | ->documentation((string)$docs) |
||||||
275 | ->kind($customField ? CompleteItemKind::FieldKind : CompleteItemKind::PropertyKind) |
||||||
276 | ->label((string)$label) |
||||||
277 | ->insertText((string)$label) |
||||||
278 | ->sortText((string)$sortPrefix . (string)$label) |
||||||
279 | ->add($this, $thisPath); |
||||||
280 | // Recurse through if this is an object |
||||||
281 | if (isset($object->$propertyName) && is_object($object->$propertyName)) { |
||||||
282 | if (!$customField && !in_array($propertyName, self::EXCLUDED_PROPERTY_NAMES, true)) { |
||||||
283 | $this->parseObject($propertyName, $object->$propertyName, $recursionDepth, $path); |
||||||
284 | } |
||||||
285 | } |
||||||
286 | } |
||||||
287 | } |
||||||
288 | } |
||||||
289 | |||||||
290 | /** |
||||||
0 ignored issues
–
show
|
|||||||
291 | * @param $object |
||||||
0 ignored issues
–
show
|
|||||||
292 | * @param DocBlockFactory $factory |
||||||
0 ignored issues
–
show
|
|||||||
293 | * @param string $path |
||||||
0 ignored issues
–
show
|
|||||||
294 | */ |
||||||
0 ignored issues
–
show
|
|||||||
295 | protected function getMethodCompletion($object, DocBlockFactory $factory, string $path): void |
||||||
296 | { |
||||||
297 | try { |
||||||
298 | $reflectionClass = new ReflectionClass($object); |
||||||
299 | } catch (ReflectionException $e) { |
||||||
300 | return; |
||||||
301 | } |
||||||
302 | $reflectionMethods = $reflectionClass->getMethods(); |
||||||
303 | foreach ($reflectionMethods as $reflectionMethod) { |
||||||
304 | $methodName = $reflectionMethod->getName(); |
||||||
305 | // Exclude some properties |
||||||
306 | $methodAllowed = true; |
||||||
307 | foreach (self::EXCLUDED_METHOD_REGEXES as $excludePattern) { |
||||||
308 | $pattern = '`' . $excludePattern . '`i'; |
||||||
309 | if (preg_match($pattern, $methodName) === 1) { |
||||||
310 | $methodAllowed = false; |
||||||
311 | } |
||||||
312 | } |
||||||
313 | // Process the method |
||||||
314 | if ($methodAllowed && $reflectionMethod->isPublic()) { |
||||||
315 | $docblock = null; |
||||||
316 | $docs = $this->getDocs($reflectionMethod, $factory); |
||||||
317 | $detail = $methodName . '('; |
||||||
318 | $params = $reflectionMethod->getParameters(); |
||||||
319 | $paramList = []; |
||||||
320 | foreach ($params as $param) { |
||||||
321 | if ($param->hasType()) { |
||||||
322 | $reflectionType = $param->getType(); |
||||||
323 | if ($reflectionType instanceof ReflectionUnionType) { |
||||||
324 | $unionTypes = $reflectionType->getTypes(); |
||||||
325 | $typeName = ''; |
||||||
326 | foreach ($unionTypes as $unionType) { |
||||||
327 | $typeName .= '|' . $unionType->getName(); |
||||||
328 | } |
||||||
329 | $typeName = trim($typeName, '|'); |
||||||
330 | $paramList[] = $typeName . ': ' . '$' . $param->getName(); |
||||||
331 | } else { |
||||||
332 | $paramList[] = $param->getType()->getName() . ': ' . '$' . $param->getName(); |
||||||
333 | } |
||||||
334 | } else { |
||||||
335 | $paramList[] = '$' . $param->getName(); |
||||||
336 | } |
||||||
337 | } |
||||||
338 | $detail .= implode(', ', $paramList) . ')'; |
||||||
339 | $thisPath = trim(implode('.', [$path, $methodName]), '.'); |
||||||
340 | $label = $methodName . '()'; |
||||||
341 | $docsPreamble = ''; |
||||||
342 | // Figure out the type |
||||||
343 | if ($docblock) { |
||||||
344 | $tags = $docblock->getTagsByName('param'); |
||||||
345 | if ($tags) { |
||||||
346 | $docsPreamble = "Parameters:\n\n"; |
||||||
347 | foreach ($tags as $tag) { |
||||||
348 | $docsPreamble .= $tag . "\n"; |
||||||
349 | } |
||||||
350 | $docsPreamble .= "\n"; |
||||||
351 | } |
||||||
352 | } |
||||||
353 | CompleteItem::create() |
||||||
354 | ->detail((string)$detail) |
||||||
355 | ->documentation((string)$docsPreamble . (string)$docs) |
||||||
356 | ->kind(CompleteItemKind::MethodKind) |
||||||
357 | ->label((string)$label) |
||||||
358 | ->insertText((string)$label) |
||||||
359 | ->sortText($this->methodSortPrefix . (string)$label) |
||||||
360 | ->add($this, $thisPath); |
||||||
361 | } |
||||||
362 | } |
||||||
363 | } |
||||||
364 | |||||||
365 | /** |
||||||
0 ignored issues
–
show
|
|||||||
366 | * @param $object |
||||||
0 ignored issues
–
show
|
|||||||
367 | * @param DocBlockFactory $factory |
||||||
0 ignored issues
–
show
|
|||||||
368 | * @param $recursionDepth |
||||||
0 ignored issues
–
show
|
|||||||
369 | * @param string $path |
||||||
0 ignored issues
–
show
|
|||||||
370 | */ |
||||||
0 ignored issues
–
show
|
|||||||
371 | protected function getBehaviorCompletion($object, DocBlockFactory $factory, $recursionDepth, string $path): void |
||||||
372 | { |
||||||
373 | if ($object instanceof Element) { |
||||||
374 | $behaviorClass = $object->getBehavior('customFields'); |
||||||
375 | if ($behaviorClass) { |
||||||
376 | $this->getPropertyCompletion($behaviorClass, $factory, $recursionDepth, $path); |
||||||
377 | } |
||||||
378 | } |
||||||
379 | } |
||||||
380 | |||||||
381 | /** |
||||||
382 | * Try to get the best documentation block we can |
||||||
383 | * |
||||||
384 | * @param ReflectionClass|ReflectionMethod $reflection |
||||||
0 ignored issues
–
show
|
|||||||
385 | * @param DocBlockFactory $factory |
||||||
0 ignored issues
–
show
|
|||||||
386 | * @return string |
||||||
0 ignored issues
–
show
|
|||||||
387 | */ |
||||||
388 | protected function getDocs($reflection, DocBlockFactory $factory): string |
||||||
389 | { |
||||||
390 | $docs = $reflection->getDocComment(); |
||||||
391 | if ($docs) { |
||||||
392 | $docblock = $factory->create($docs); |
||||||
393 | $summary = $docblock->getSummary(); |
||||||
394 | if (!empty($summary)) { |
||||||
395 | $docs = $summary; |
||||||
396 | } |
||||||
397 | $description = $docblock->getDescription()->render(); |
||||||
398 | if (!empty($description)) { |
||||||
399 | $docs = $description; |
||||||
400 | } |
||||||
401 | } |
||||||
402 | |||||||
403 | return $docs ?: ''; |
||||||
404 | } |
||||||
405 | } |
||||||
406 |