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.
Completed
Pull Request — master (#197)
by joseph
60:04 queued 07:22
created

DoctrineStaticMeta::getReflectionHelper()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 7
Code Lines 3

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 3
CRAP Score 2.0625

Importance

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