GitHub Access Token became invalid

It seems like the GitHub access token used for retrieving details about this repository from GitHub became invalid. This might prevent certain types of inspections from being run (in particular, everything related to pull requests).
Please ask an admin of your repository to re-new the access token on this website.

DoctrineStaticMeta::getTypesFromVarComment()   B
last analyzed

Complexity

Conditions 6
Paths 7

Size

Total Lines 31
Code Lines 22

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 17
CRAP Score 6.6393

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 6
eloc 22
nc 7
nop 2
dl 0
loc 31
ccs 17
cts 23
cp 0.7391
crap 6.6393
rs 8.9457
c 1
b 0
f 0
1
<?php
2
3
declare(strict_types=1);
4
5
namespace EdmondsCommerce\DoctrineStaticMeta;
6
7
use Doctrine\Common\Inflector\Inflector;
8
use Doctrine\ORM\Mapping\Builder\ClassMetadataBuilder;
9
use Doctrine\ORM\Mapping\ClassMetadata;
10
use Doctrine\ORM\Mapping\ClassMetadataInfo;
11
use EdmondsCommerce\DoctrineStaticMeta\CodeGeneration\Generator\AbstractGenerator;
12
use EdmondsCommerce\DoctrineStaticMeta\CodeGeneration\NamespaceHelper;
13
use EdmondsCommerce\DoctrineStaticMeta\CodeGeneration\ReflectionHelper;
14
use EdmondsCommerce\DoctrineStaticMeta\DoctrineStaticMeta\RequiredRelation;
15
use EdmondsCommerce\DoctrineStaticMeta\Entity\Interfaces\UsesPHPMetaDataInterface;
16
use EdmondsCommerce\DoctrineStaticMeta\Exception\DoctrineStaticMetaException;
17
use Exception;
18
use ReflectionException;
19
use RuntimeException;
20
use ts\Reflection\ReflectionClass;
21
use ts\Reflection\ReflectionMethod;
22
23
use function array_pop;
24
use function explode;
25
use function lcfirst;
26
use function preg_match;
27
use function preg_replace;
28
use function trim;
29
30
/**
31
 * @SuppressWarnings(PHPMD.CouplingBetweenObjects)
32
 * @SuppressWarnings(PHPMD.ExcessiveClassComplexity)
33
 * @SuppressWarnings(PHPMD.TooManyPublicMethods)
34
 */
35
class DoctrineStaticMeta
36
{
37
    public const DSM_INIT_METHOD_PREFIX = 'dsmInit';
38
39
    /**
40
     * @var NamespaceHelper
41
     */
42
    private static $namespaceHelper;
43
    /**
44
     * @var ReflectionHelper
45
     */
46
    private static $reflectionHelper;
47
    /**
48
     * @var array
49
     */
50
    private $embeddableProperties;
51
    /**
52
     * @var array
53
     */
54
    private $getters;
55
    /**
56
     * @var ClassMetadata|\Doctrine\Common\Persistence\Mapping\ClassMetadata|ClassMetadataInfo
57
     */
58
    private $metaData;
59
    /**
60
     * @var string
61
     */
62
    private $plural;
63
    /**
64
     * @var ReflectionClass
65
     */
66
    private $reflectionClass;
67
    /**
68
     * @var RequiredRelation[]
69
     */
70
    private $requiredRelationProperties;
71
    /**
72
     * @var array
73
     */
74
    private $setters;
75
    /**
76
     * @var string
77
     */
78
    private $singular;
79
    /**
80
     * @var array|null
81
     */
82
    private $staticMethods;
83
84
    /**
85
     * DoctrineStaticMeta constructor.
86
     *
87
     * @param string $entityFqn
88
     *
89
     * @throws DoctrineStaticMetaException
90
     * @throws ReflectionException
91
     */
92 53
    public function __construct(string $entityFqn)
93
    {
94 53
        $this->reflectionClass = new ReflectionClass($entityFqn);
95 53
        $this->runDsmInitMethods();
96 53
    }
97
98 53
    private function runDsmInitMethods(): void
99
    {
100 53
        $methodName = '__no_method__';
101
        try {
102 53
            $staticMethods = $this->getStaticMethods();
103
            //now loop through and call them
104 53
            foreach ($staticMethods as $method) {
105 53
                $methodName = $method->getName();
106
                if (
107 53
                    \ts\stringStartsWith($methodName, self::DSM_INIT_METHOD_PREFIX)
108
                ) {
109 2
                    $method->setAccessible(true);
110 2
                    $method->invokeArgs(null, [$this]);
111
                }
112
            }
113
        } catch (Exception $e) {
114
            throw new DoctrineStaticMetaException(
115
                'Exception in ' . __METHOD__ . ' for '
116
                . $this->reflectionClass->getName() . "::$methodName\n\n"
117
                . $e->getMessage(),
118
                $e->getCode(),
119
                $e
120
            );
121
        }
122 53
    }
123
124
    /**
125
     * Get an array of all static methods implemented by the current class
126
     *
127
     * Merges trait methods
128
     * Filters out this trait
129
     *
130
     * @return array|ReflectionMethod[]
131
     * @throws ReflectionException
132
     */
133 53
    public function getStaticMethods(): array
134
    {
135 53
        if (null !== $this->staticMethods) {
136 1
            return $this->staticMethods;
137
        }
138 53
        $this->staticMethods = $this->reflectionClass->getMethods(
139 53
            \ReflectionMethod::IS_STATIC
140
        );
141
142 53
        return $this->staticMethods;
143
    }
144
145 2
    public function setRequiredRelationProperty(RequiredRelation $requiredRelation): self
146
    {
147 2
        $this->requiredRelationProperties[$requiredRelation->getPropertyName()] = $requiredRelation;
148
149 2
        return $this;
150
    }
151
152
    /**
153
     * @return RequiredRelation[]
154
     */
155 2
    public function getRequiredRelationProperties(): array
156
    {
157 2
        return $this->requiredRelationProperties ?? [];
158
    }
159
160 1
    public function getMetaData(): ClassMetadata
161
    {
162 1
        if ($this->metaData instanceof ClassMetadata) {
163 1
            return $this->metaData;
164
        }
165
        $this->metaData = new ClassMetadata($this->reflectionClass->getName());
166
        $this->buildMetaData();
167
168
        return $this->metaData;
169
    }
170
171 1
    public function setMetaData(ClassMetadata $metaData): self
172
    {
173 1
        $this->metaData = $metaData;
174
175 1
        return $this;
176
    }
177
178
    public function buildMetaData(): void
179
    {
180
        if (false === $this->metaData instanceof ClassMetadataInfo) {
181
            throw new RuntimeException('Invalid meta data class ' . \ts\print_r($this->metaData, true));
182
        }
183
        $builder = new ClassMetadataBuilder($this->metaData);
184
        $this->loadDoctrineMetaData($builder, UsesPHPMetaDataInterface::METHOD_PREFIX_GET_PROPERTY_DOCTRINE_META);
185
        $this->loadDoctrineMetaData($builder, UsesPHPMetaDataInterface::METHOD_PREFIX_GET_CLASS_DOCTRINE_META);
186
        $this->setTableName($builder);
187
        $this->setChangeTrackingPolicy($builder);
188
        $this->setCustomRepositoryClass($builder);
189
    }
190
191
    /**
192
     * This method will reflect on the entity class and pull out all the methods that begin with
193
     * UsesPHPMetaDataInterface::METHOD_PREFIX_GET_PROPERTY_DOCTRINE_META
194
     *
195
     * Once it has an array of methods, it calls them all, passing in the $builder
196
     *
197
     * @param ClassMetadataBuilder $builder
198
     * @param string               $methodPrefix
199
     *
200
     * @throws DoctrineStaticMetaException
201
     * @SuppressWarnings(PHPMD.StaticAccess)
202
     */
203
    private function loadDoctrineMetaData(ClassMetadataBuilder $builder, string $methodPrefix): void
204
    {
205
        $methodName = '__no_method__';
206
        try {
207
            $staticMethods = $this->getStaticMethods();
208
            //now loop through and call them
209
            foreach ($staticMethods as $method) {
210
                $methodName = $method->getName();
211
                if (
212
                    0 === stripos(
213
                        $methodName,
214
                        $methodPrefix
215
                    )
216
                ) {
217
                    $method->setAccessible(true);
218
                    $method->invokeArgs(null, [$builder]);
219
                }
220
            }
221
        } catch (Exception $e) {
222
            throw new DoctrineStaticMetaException(
223
                'Exception in ' . __METHOD__ . ' for '
224
                . $this->reflectionClass->getName() . "::$methodName\n\n"
225
                . $e->getMessage(),
226
                $e->getCode(),
227
                $e
228
            );
229
        }
230
    }
231
232
    /**
233
     * Sets the table name for the class
234
     *
235
     * @param ClassMetadataBuilder $builder
236
     *
237
     * @SuppressWarnings(PHPMD.StaticAccess)
238
     */
239
    private function setTableName(ClassMetadataBuilder $builder): void
240
    {
241
        $tableName = MappingHelper::getTableNameForEntityFqn($this->reflectionClass->getName());
242
        $builder->setTable($tableName);
243
    }
244
245
    /**
246
     * Setting the change policy to be Notify - best performance
247
     *
248
     * @see http://doctrine-orm.readthedocs.io/en/latest/reference/change-tracking-policies.html
249
     *
250
     * @param ClassMetadataBuilder $builder
251
     */
252
    public function setChangeTrackingPolicy(ClassMetadataBuilder $builder): void
253
    {
254
        $builder->setChangeTrackingPolicyNotify();
255
    }
256
257
    private function setCustomRepositoryClass(ClassMetadataBuilder $builder): void
258
    {
259
        $repositoryClassName = (new NamespaceHelper())->getRepositoryqnFromEntityFqn($this->reflectionClass->getName());
260
        $builder->setCustomRepositoryClass($repositoryClassName);
261
    }
262
263
    /**
264
     * Get an array of property names that contain embeddable objects
265
     *
266
     * @return array
267
     * @throws ReflectionException
268
     */
269 1
    public function getEmbeddableProperties(): array
270
    {
271 1
        if (null !== $this->embeddableProperties) {
272
            return $this->embeddableProperties;
273
        }
274 1
        $traits = $this->reflectionClass->getTraits();
275 1
        $return = [];
276 1
        foreach ($traits as $traitName => $traitReflection) {
277 1
            if (\ts\stringContains($traitName, '\\Entity\\Embeddable\\Traits')) {
278 1
                $property                     = $traitReflection->getProperties()[0]->getName();
279 1
                $embeddableObjectInterfaceFqn = $this->getTypesFromVarComment(
280 1
                    $property,
281 1
                    $this->getReflectionHelper()->getTraitProvidingProperty($traitReflection, $property)->getFileName()
282 1
                )[0];
283 1
                $embeddableObject             = $this->getNamespaceHelper()
284 1
                                                     ->getEmbeddableObjectFqnFromEmbeddableObjectInterfaceFqn(
285 1
                                                         $embeddableObjectInterfaceFqn
286
                                                     );
287 1
                $return[$property]            = $embeddableObject;
288
            }
289
        }
290
291 1
        return $return;
292
    }
293
294
    /**
295
     * Parse the docblock for a property and get the type, then read the source code to resolve the short type to the
296
     * FQN of the type. Roll on PHP 7.3
297
     *
298
     * @param string $property
299
     *
300
     * @param string $filename
301
     *
302
     * @return array
303
     */
304 1
    private function getTypesFromVarComment(string $property, string $filename): array
305
    {
306 1
        $docComment = $this->reflectionClass->getProperty($property)->getDocComment();
307 1
        preg_match('%@var\s*?(.+)%', $docComment, $matches);
308 1
        $traitCode = \ts\file_get_contents($filename);
309 1
        $types     = explode('|', $matches[1]);
310 1
        $return    = [];
311 1
        foreach ($types as $type) {
312 1
            $type = trim($type);
313 1
            if ('null' === $type) {
314
                continue;
315
            }
316 1
            if ('ArrayCollection' === $type) {
317
                continue;
318
            }
319 1
            $arrayNotation = '';
320 1
            if ('[]' === substr($type, -2)) {
321
                $type          = substr($type, 0, -2);
322
                $arrayNotation = '[]';
323
            }
324 1
            $pattern = "%^use (.+?)\\\\${type}(;| |\[)%m";
325 1
            preg_match($pattern, $traitCode, $matches);
326 1
            if (!isset($matches[1])) {
327
                throw new RuntimeException(
328
                    'Failed finding match for type ' . $type . ' in ' . $filename
329
                );
330
            }
331 1
            $return[] = $matches[1] . '\\' . $type . $arrayNotation;
332
        }
333
334 1
        return $return;
335
    }
336
337 1
    private function getReflectionHelper(): ReflectionHelper
338
    {
339 1
        if (null === self::$reflectionHelper) {
340
            self::$reflectionHelper = new ReflectionHelper($this->getNamespaceHelper());
341
        }
342
343 1
        return self::$reflectionHelper;
344
    }
345
346 1
    private function getNamespaceHelper(): NamespaceHelper
347
    {
348 1
        if (null === self::$namespaceHelper) {
349
            self::$namespaceHelper = new NamespaceHelper();
350
        }
351
352 1
        return self::$namespaceHelper;
353
    }
354
355
    /**
356
     * Get the property name the Entity is mapped by when plural
357
     *
358
     * Override it in your entity class if you are using an Entity class name that doesn't pluralize nicely
359
     *
360
     * @return string
361
     * @throws DoctrineStaticMetaException
362
     * @SuppressWarnings(PHPMD.StaticAccess)
363
     */
364 1
    public function getPlural(): string
365
    {
366
        try {
367 1
            if (null === $this->plural) {
368 1
                $singular     = $this->getSingular();
369 1
                $this->plural = Inflector::pluralize($singular);
370
            }
371
372 1
            return $this->plural;
373
        } catch (Exception $e) {
374
            throw new DoctrineStaticMetaException(
375
                'Exception in ' . __METHOD__ . ': ' . $e->getMessage(),
376
                $e->getCode(),
377
                $e
378
            );
379
        }
380
    }
381
382
    /**
383
     * Get the property the name the Entity is mapped by when singular
384
     *
385
     * @return string
386
     * @throws DoctrineStaticMetaException
387
     * @SuppressWarnings(PHPMD.StaticAccess)
388
     */
389 2
    public function getSingular(): string
390
    {
391
        try {
392 2
            if (null === $this->singular) {
393 2
                $reflectionClass = $this->getReflectionClass();
394
395 2
                $shortName         = $reflectionClass->getShortName();
396 2
                $singularShortName = MappingHelper::singularize($shortName);
397
398 2
                $namespaceName   = $reflectionClass->getNamespaceName();
399 2
                $namespaceParts  = explode(AbstractGenerator::ENTITIES_FOLDER_NAME, $namespaceName);
400 2
                $entityNamespace = array_pop($namespaceParts);
401
402 2
                $namespacedShortName = preg_replace(
403 2
                    '/\\\\/',
404 2
                    '',
405 2
                    $entityNamespace . $singularShortName
406
                );
407
408 2
                $this->singular = lcfirst($namespacedShortName);
409
            }
410
411 2
            return $this->singular;
412
        } catch (Exception $e) {
413
            throw new DoctrineStaticMetaException(
414
                'Exception in ' . __METHOD__ . ': ' . $e->getMessage(),
415
                $e->getCode(),
416
                $e
417
            );
418
        }
419
    }
420
421
    /**
422
     * @return ReflectionClass
423
     */
424 20
    public function getReflectionClass(): ReflectionClass
425
    {
426 20
        return $this->reflectionClass;
427
    }
428
429 14
    public function getSetterNameFromPropertyName(string $property): ?string
430
    {
431 14
        foreach ($this->getSetters() as $setter) {
432 14
            if (preg_match('%^(set|add)' . $property . '%i', $setter)) {
433 14
                return $setter;
434
            }
435
        }
436
437
        return null;
438
    }
439
440
    /**
441
     * Get an array of setters by name
442
     *
443
     * @return array|string[]
444
     * @throws ReflectionException
445
     */
446 15
    public function getSetters(): array
447
    {
448 15
        if (null !== $this->setters) {
449
            return $this->setters;
450
        }
451
        $skip            = [
452 15
            'addPropertyChangedListener'     => true,
453
            'setEntityCollectionAndNotify'   => true,
454
            'addToEntityCollectionAndNotify' => true,
455
            'setEntityAndNotify'             => true,
456
        ];
457 15
        $this->setters   = [];
458 15
        $reflectionClass = $this->getReflectionClass();
459
        foreach (
460 15
            $reflectionClass->getMethods(
461 15
                \ReflectionMethod::IS_PRIVATE | \ReflectionMethod::IS_PUBLIC
462
            ) as $method
463
        ) {
464 15
            $methodName = $method->getName();
465 15
            if (isset($skip[$methodName])) {
466 15
                continue;
467
            }
468 15
            if (\ts\stringStartsWith($methodName, 'set')) {
469 15
                $this->setters[$this->getGetterForSetter($methodName)] = $methodName;
470 15
                continue;
471
            }
472
        }
473
474 15
        return $this->setters;
475
    }
476
477 15
    private function getGetterForSetter(string $setterName): string
478
    {
479 15
        $propertyName    = $this->getPropertyNameFromSetterName($setterName);
480 15
        $matchingGetters = [];
481 15
        foreach ($this->getGetters() as $getterName) {
482 15
            $getterPropertyName = $this->getPropertyNameFromGetterName($getterName);
483 15
            if (strtolower($getterPropertyName) === strtolower($propertyName)) {
484 15
                $matchingGetters[] = $getterName;
485
            }
486
        }
487 15
        if (count($matchingGetters) !== 1) {
488
            throw new RuntimeException(
489
                'Found either less or more than one matching getter for ' .
490
                $propertyName . ': ' . print_r($matchingGetters, true)
491
                . "\n Current Entity: " . $this->getReflectionClass()->getName()
492
            );
493
        }
494
495 15
        return current($matchingGetters);
496
    }
497
498 29
    public function getPropertyNameFromSetterName(string $setterName): string
499
    {
500 29
        $propertyName = preg_replace('%^(set|add)(.+)%', '$2', $setterName);
501 29
        $propertyName = lcfirst($propertyName);
502
503 29
        return $propertyName;
504
    }
505
506
    /**
507
     * Get an array of getters by name
508
     *
509
     * @return array|string[]
510
     * @throws ReflectionException
511
     */
512 16
    public function getGetters(): array
513
    {
514 16
        if (null !== $this->getters) {
515 15
            return $this->getters;
516
        }
517
        $skip = [
518 16
            'getEntityFqn'          => true,
519
            'getDoctrineStaticMeta' => true,
520
            'isValid'               => true,
521
            'getValidator'          => true,
522
        ];
523
524 16
        $this->getters   = [];
525 16
        $reflectionClass = $this->getReflectionClass();
526 16
        foreach ($reflectionClass->getMethods(\ReflectionMethod::IS_PUBLIC) as $method) {
527 16
            $methodName = $method->getName();
528 16
            if (isset($skip[$methodName])) {
529 16
                continue;
530
            }
531 16
            if (\ts\stringStartsWith($methodName, 'get')) {
532 16
                $this->getters[] = $methodName;
533 16
                continue;
534
            }
535 16
            if (\ts\stringStartsWith($methodName, 'is')) {
536 16
                $this->getters[] = $methodName;
537 16
                continue;
538
            }
539 16
            if (\ts\stringStartsWith($methodName, 'has')) {
540
                $this->getters[] = $methodName;
541
                continue;
542
            }
543
        }
544
545 16
        return $this->getters;
546
    }
547
548 29
    public function getPropertyNameFromGetterName(string $getterName): string
549
    {
550 29
        $propertyName = preg_replace('%^(get|is|has)(.+)%', '$2', $getterName);
551 29
        $propertyName = lcfirst($propertyName);
552
553 29
        return $propertyName;
554
    }
555
556
    /**
557
     * Get the short name (without fully qualified namespace) of the current Entity
558
     *
559
     * @return string
560
     */
561 1
    public function getShortName(): string
562
    {
563 1
        $reflectionClass = $this->getReflectionClass();
564
565 1
        return $reflectionClass->getShortName();
566
    }
567
}
568