ObjectParserAutocomplete::getClassCompletion()   A
last analyzed

Complexity

Conditions 2
Paths 2

Size

Total Lines 17
Code Lines 13

Duplication

Lines 0
Ratio 0 %

Importance

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