Passed
Push — develop ( 6de964...498c1a )
by Andrew
04:34
created

ObjectParserAutocomplete::parseObject()   B

Complexity

Conditions 7
Paths 33

Size

Total Lines 30
Code Lines 15

Duplication

Lines 0
Ratio 0 %

Importance

Changes 2
Bugs 0 Features 0
Metric Value
cc 7
eloc 15
c 2
b 0
f 0
nc 33
nop 4
dl 0
loc 30
rs 8.8333
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
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\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
Coding Style introduced by
Missing short description in doc comment
Loading history...
26
 * @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...
27
 * @package   twigfield
0 ignored issues
show
Coding Style introduced by
Package name "twigfield" is not valid; consider "Twigfield" instead
Loading history...
Coding Style introduced by
Tag value for @package tag indented incorrectly; expected 1 spaces but found 3
Loading history...
28
 * @since     1.0.12
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...
29
 */
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...
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
    const CUSTOM_PROPERTY_SORT_PREFIX = '~';
56
    const PROPERTY_SORT_PREFIX = '~~';
57
    const METHOD_SORT_PREFIX = '~~~';
58
59
    // Public Properties
60
    // =========================================================================
61
62
    /**
0 ignored issues
show
Coding Style introduced by
Missing short description in doc comment
Loading history...
63
     * @var bool If the class itself should be parsed for complete items
64
     */
65
    public $parseClass = true;
66
67
    /**
0 ignored issues
show
Coding Style introduced by
Missing short description in doc comment
Loading history...
68
     * @var bool If any ServiceLocator components should be parsed for complete items
69
     */
70
    public $parseComponents = true;
71
72
    /**
0 ignored issues
show
Coding Style introduced by
Missing short description in doc comment
Loading history...
73
     * @var bool If the class properties should be parsed for complete items
74
     */
75
    public $parseProperties = true;
76
77
    /**
0 ignored issues
show
Coding Style introduced by
Missing short description in doc comment
Loading history...
78
     * @var bool If the class methods should be parsed for complete items
79
     */
80
    public $parseMethods = true;
81
82
    /**
0 ignored issues
show
Coding Style introduced by
Missing short description in doc comment
Loading history...
83
     * @var bool If the class behaviors should be parsed for complete items
84
     */
85
    public $parseBehaviors = true;
86
87
    // Public Methods
88
    // =========================================================================
89
90
    /**
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...
91
     * @inerhitdoc
92
     */
0 ignored issues
show
Coding Style introduced by
Missing @return tag in function comment
Loading history...
93
    public function parseObject(string $name, $object, int $recursionDepth, string $path = ''): void
94
    {
95
        // Only recurse `RECURSION_DEPTH_LIMIT` deep
96
        if ($recursionDepth > self::RECURSION_DEPTH_LIMIT) {
97
            return;
98
        }
99
        $recursionDepth++;
100
        // Create the docblock factory
101
        $factory = DocBlockFactory::createInstance();
102
103
        $path = trim(implode('.', [$path, $name]), '.');
104
        // The class itself
105
        if ($this->parseClass) {
106
            $this->getClassCompletion($object, $factory, $name, $path);
107
        }
108
        // ServiceLocator Components
109
        if ($this->parseComponents) {
110
            $this->getComponentCompletion($object, $recursionDepth, $path);
111
        }
112
        // Class properties
113
        if ($this->parseProperties) {
114
            $this->getPropertyCompletion($object, $factory, $recursionDepth, $path);
115
        }
116
        // Class methods
117
        if ($this->parseMethods) {
118
            $this->getMethodCompletion($object, $factory, $path);
119
        }
120
        // Behavior properties
121
        if ($this->parseBeaviors) {
0 ignored issues
show
Bug Best Practice introduced by
The property parseBeaviors does not exist on nystudio107\twigfield\ba...bjectParserAutocomplete. Since you implemented __get, consider adding a @property annotation.
Loading history...
122
            $this->getBehaviorCompletion($object, $factory, $recursionDepth, $path);
123
        }
124
    }
125
126
    // Protected Methods
127
    // =========================================================================
128
129
    /**
0 ignored issues
show
Coding Style introduced by
Missing short description in doc comment
Loading history...
130
     * @param $object
0 ignored issues
show
Coding Style introduced by
Missing parameter comment
Loading history...
131
     * @param DocBlockFactory $factory
0 ignored issues
show
Coding Style introduced by
Missing parameter comment
Loading history...
132
     * @param string $name
0 ignored issues
show
Coding Style introduced by
Missing parameter comment
Loading history...
Coding Style introduced by
Expected 10 spaces after parameter type; 1 found
Loading history...
133
     * @param $path
0 ignored issues
show
Coding Style introduced by
Missing parameter comment
Loading history...
134
     */
0 ignored issues
show
Coding Style introduced by
Missing @return tag in function comment
Loading history...
135
    protected function getClassCompletion($object, DocBlockFactory $factory, string $name, $path): void
136
    {
137
        try {
138
            $reflectionClass = new ReflectionClass($object);
139
        } catch (ReflectionException $e) {
140
            return;
141
        }
142
        // Information on the class itself
143
        $className = $reflectionClass->getName();
144
        $docs = $this->getDocs($reflectionClass, $factory);
145
        CompleteItem::create()
146
            ->detail((string)$className)
147
            ->documentation((string)$docs)
148
            ->kind(CompleteItemKind::ClassKind)
149
            ->label((string)$name)
150
            ->insertText((string)$name)
151
            ->add($this, $path);
152
    }
153
154
    /**
0 ignored issues
show
Coding Style introduced by
Missing short description in doc comment
Loading history...
155
     * @param $object
0 ignored issues
show
Coding Style introduced by
Missing parameter comment
Loading history...
156
     * @param $recursionDepth
0 ignored issues
show
Coding Style introduced by
Missing parameter comment
Loading history...
157
     * @param $path
0 ignored issues
show
Coding Style introduced by
Missing parameter comment
Loading history...
158
     */
0 ignored issues
show
Coding Style introduced by
Missing @return tag in function comment
Loading history...
159
    protected function getComponentCompletion($object, $recursionDepth, $path): void
160
    {
161
        if ($object instanceof ServiceLocator) {
162
            foreach ($object->getComponents() as $key => $value) {
163
                $componentObject = null;
0 ignored issues
show
Unused Code introduced by
The assignment to $componentObject is dead and can be removed.
Loading history...
164
                try {
165
                    $componentObject = $object->get($key);
166
                } catch (InvalidConfigException $e) {
167
                    // That's okay
168
                }
169
                if ($componentObject) {
170
                    $this->parseObject($key, $componentObject, $recursionDepth, $path);
171
                }
172
            }
173
        }
174
    }
175
176
    /**
0 ignored issues
show
Coding Style introduced by
Missing short description in doc comment
Loading history...
177
     * @param $object
0 ignored issues
show
Coding Style introduced by
Missing parameter comment
Loading history...
178
     * @param DocBlockFactory $factory
0 ignored issues
show
Coding Style introduced by
Missing parameter comment
Loading history...
179
     * @param $recursionDepth
0 ignored issues
show
Coding Style introduced by
Missing parameter comment
Loading history...
180
     * @param string $path
0 ignored issues
show
Coding Style introduced by
Missing parameter comment
Loading history...
Coding Style introduced by
Expected 10 spaces after parameter type; 1 found
Loading history...
181
     */
0 ignored issues
show
Coding Style introduced by
Missing @return tag in function comment
Loading history...
182
    protected function getPropertyCompletion($object, DocBlockFactory $factory, $recursionDepth, string $path): void
183
    {
184
        try {
185
            $reflectionClass = new ReflectionClass($object);
186
        } catch (ReflectionException $e) {
187
            return;
188
        }
189
        $reflectionProperties = $reflectionClass->getProperties();
190
        $customField = false;
191
        if ($object instanceof Behavior) {
192
            $customField = true;
193
        }
194
        $sortPrefix = $customField ? self::CUSTOM_PROPERTY_SORT_PREFIX : self::PROPERTY_SORT_PREFIX;
195
        foreach ($reflectionProperties as $reflectionProperty) {
196
            $propertyName = $reflectionProperty->getName();
197
            // Exclude some properties
198
            $propertyAllowed = true;
199
            foreach (self::EXCLUDED_PROPERTY_REGEXES as $excludePattern) {
200
                $pattern = '`' . $excludePattern . '`i';
201
                if (preg_match($pattern, $propertyName) === 1) {
202
                    $propertyAllowed = false;
203
                }
204
            }
205
            if (in_array($propertyName, self::EXCLUDED_PROPERTY_NAMES, true)) {
206
                $propertyAllowed = false;
207
            }
208
            if ($customField && in_array($propertyName, self::EXCLUDED_BEHAVIOR_NAMES, true)) {
209
                $propertyAllowed = false;
210
            }
211
            // Process the property
212
            if ($propertyAllowed && $reflectionProperty->isPublic()) {
213
                $detail = "Property";
214
                $docblock = null;
215
                $docs = $reflectionProperty->getDocComment();
216
                if ($docs) {
217
                    $docblock = $factory->create($docs);
218
                    $docs = '';
219
                    $summary = $docblock->getSummary();
220
                    if (!empty($summary)) {
221
                        $docs = $summary;
222
                    }
223
                    $description = $docblock->getDescription()->render();
224
                    if (!empty($description)) {
225
                        $docs = $description;
226
                    }
227
                }
228
                // Figure out the type
229
                if ($docblock) {
230
                    $tag = $docblock->getTagsByName('var');
231
                    if ($tag && isset($tag[0])) {
232
                        $docs = $tag[0];
233
                    }
234
                }
235
                if (preg_match('/@var\s+([^\s]+)/', $docs, $matches)) {
236
                    list(, $type) = $matches;
237
                    $detail = $type;
238
                }
239
                if ($detail === "Property") {
240
                    if ((PHP_MAJOR_VERSION >= 7 && PHP_MINOR_VERSION >= 4) || (PHP_MAJOR_VERSION >= 8)) {
241
                        if ($reflectionProperty->hasType()) {
242
                            $reflectionType = $reflectionProperty->getType();
243
                            if ($reflectionType instanceof ReflectionNamedType) {
0 ignored issues
show
Bug introduced by
The type nystudio107\twigfield\base\ReflectionNamedType was not found. Did you mean ReflectionNamedType? If so, make sure to prefix the type with \.
Loading history...
244
                                $type = $reflectionType->getName();
0 ignored issues
show
Bug introduced by
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 ignore-call  annotation

244
                                /** @scrutinizer ignore-call */ 
245
                                $type = $reflectionType->getName();
Loading history...
245
                                $detail = $type;
246
                            }
247
                        }
248
                        if ((PHP_MAJOR_VERSION >= 8) && $reflectionProperty->hasDefaultValue()) {
249
                            $value = $reflectionProperty->getDefaultValue();
250
                            if (is_array($value)) {
251
                                $value = json_encode($value);
252
                            }
253
                            if (!empty($value)) {
254
                                $detail = (string)$value;
255
                            }
256
                        }
257
                    }
258
                }
259
                $thisPath = trim(implode('.', [$path, $propertyName]), '.');
260
                $label = $propertyName;
261
                CompleteItem::create()
262
                    ->detail((string)$detail)
263
                    ->documentation((string)$docs)
264
                    ->kind($customField ? CompleteItemKind::FieldKind : CompleteItemKind::PropertyKind)
265
                    ->label((string)$label)
266
                    ->insertText((string)$label)
267
                    ->sortText((string)$sortPrefix . (string)$label)
268
                    ->add($this, $thisPath);
269
                // Recurse through if this is an object
270
                if (isset($object->$propertyName) && is_object($object->$propertyName)) {
271
                    if (!$customField && !in_array($propertyName, self::EXCLUDED_PROPERTY_NAMES, true)) {
272
                        $this->parseObject($propertyName, $object->$propertyName, $recursionDepth, $path);
273
                    }
274
                }
275
            }
276
        }
277
    }
278
279
    /**
0 ignored issues
show
Coding Style introduced by
Missing short description in doc comment
Loading history...
280
     * @param $object
0 ignored issues
show
Coding Style introduced by
Missing parameter comment
Loading history...
281
     * @param DocBlockFactory $factory
0 ignored issues
show
Coding Style introduced by
Missing parameter comment
Loading history...
282
     * @param string $path
0 ignored issues
show
Coding Style introduced by
Missing parameter comment
Loading history...
Coding Style introduced by
Expected 10 spaces after parameter type; 1 found
Loading history...
283
     */
0 ignored issues
show
Coding Style introduced by
Missing @return tag in function comment
Loading history...
284
    protected function getMethodCompletion($object, DocBlockFactory $factory, string $path): void
285
    {
286
        try {
287
            $reflectionClass = new ReflectionClass($object);
288
        } catch (ReflectionException $e) {
289
            return;
290
        }
291
        $reflectionMethods = $reflectionClass->getMethods();
292
        foreach ($reflectionMethods as $reflectionMethod) {
293
            $methodName = $reflectionMethod->getName();
294
            // Exclude some properties
295
            $methodAllowed = true;
296
            foreach (self::EXCLUDED_METHOD_REGEXES as $excludePattern) {
297
                $pattern = '`' . $excludePattern . '`i';
298
                if (preg_match($pattern, $methodName) === 1) {
299
                    $methodAllowed = false;
300
                }
301
            }
302
            // Process the method
303
            if ($methodAllowed && $reflectionMethod->isPublic()) {
304
                $docblock = null;
305
                $docs = $this->getDocs($reflectionMethod, $factory);
306
                $detail = $methodName . '(';
307
                $params = $reflectionMethod->getParameters();
308
                $paramList = [];
309
                foreach ($params as $param) {
310
                    if ($param->hasType()) {
311
                        $reflectionType = $param->getType();
312
                        if ($reflectionType instanceof ReflectionUnionType) {
313
                            $unionTypes = $reflectionType->getTypes();
314
                            $typeName = '';
315
                            foreach ($unionTypes as $unionType) {
316
                                $typeName .= '|' . $unionType->getName();
317
                            }
318
                            $typeName = trim($typeName, '|');
319
                            $paramList[] = $typeName . ': ' . '$' . $param->getName();
320
                        } else {
321
                            $paramList[] = $param->getType()->getName() . ': ' . '$' . $param->getName();
322
                        }
323
                    } else {
324
                        $paramList[] = '$' . $param->getName();
325
                    }
326
                }
327
                $detail .= implode(', ', $paramList) . ')';
328
                $thisPath = trim(implode('.', [$path, $methodName]), '.');
329
                $label = $methodName . '()';
330
                $docsPreamble = '';
331
                // Figure out the type
332
                if ($docblock) {
333
                    $tags = $docblock->getTagsByName('param');
334
                    if ($tags) {
335
                        $docsPreamble = "Parameters:\n\n";
336
                        foreach ($tags as $tag) {
337
                            $docsPreamble .= $tag . "\n";
338
                        }
339
                        $docsPreamble .= "\n";
340
                    }
341
                }
342
                CompleteItem::create()
343
                    ->detail((string)$detail)
344
                    ->documentation((string)$docsPreamble . (string)$docs)
345
                    ->kind(CompleteItemKind::MethodKind)
346
                    ->label((string)$label)
347
                    ->insertText((string)$label)
348
                    ->sortText(self::METHOD_SORT_PREFIX . (string)$label)
349
                    ->add($this, $thisPath);
350
            }
351
        }
352
    }
353
354
    /**
0 ignored issues
show
Coding Style introduced by
Missing short description in doc comment
Loading history...
355
     * @param $object
0 ignored issues
show
Coding Style introduced by
Missing parameter comment
Loading history...
356
     * @param DocBlockFactory $factory
0 ignored issues
show
Coding Style introduced by
Missing parameter comment
Loading history...
357
     * @param $recursionDepth
0 ignored issues
show
Coding Style introduced by
Missing parameter comment
Loading history...
358
     * @param string $path
0 ignored issues
show
Coding Style introduced by
Missing parameter comment
Loading history...
Coding Style introduced by
Expected 10 spaces after parameter type; 1 found
Loading history...
359
     */
0 ignored issues
show
Coding Style introduced by
Missing @return tag in function comment
Loading history...
360
    protected function getBehaviorCompletion($object, DocBlockFactory $factory, $recursionDepth, string $path): void
361
    {
362
        if ($object instanceof Element) {
363
            $behaviorClass = $object->getBehavior('customFields');
364
            if ($behaviorClass) {
365
                $this->getPropertyCompletion($behaviorClass, $factory, $recursionDepth, $path);
366
            }
367
        }
368
    }
369
370
    /**
371
     * Try to get the best documentation block we can
372
     *
373
     * @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...
374
     * @param DocBlockFactory $factory
0 ignored issues
show
Coding Style introduced by
Missing parameter comment
Loading history...
Coding Style introduced by
Expected 18 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...
375
     * @return string
0 ignored issues
show
Coding Style introduced by
Tag @return cannot be grouped with parameter tags in a doc comment
Loading history...
376
     */
377
    protected function getDocs($reflection, DocBlockFactory $factory): string
378
    {
379
        $docs = $reflection->getDocComment();
380
        if ($docs) {
381
            $docblock = $factory->create($docs);
382
            $summary = $docblock->getSummary();
383
            if (!empty($summary)) {
384
                $docs = $summary;
385
            }
386
            $description = $docblock->getDescription()->render();
387
            if (!empty($description)) {
388
                $docs = $description;
389
            }
390
        }
391
392
        return $docs ?: '';
393
    }
394
}
395