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 (#191)
by joseph
16:39 queued 23s
created

DoctrineStaticMeta   D

Complexity

Total Complexity 59

Size/Duplication

Total Lines 503
Duplicated Lines 0 %

Test Coverage

Coverage 69.27%

Importance

Changes 0
Metric Value
eloc 200
dl 0
loc 503
ccs 142
cts 205
cp 0.6927
rs 4.08
c 0
b 0
f 0
wmc 59

23 Methods

Rating   Name   Duplication   Size   Complexity  
A setTableName() 0 4 1
A buildMetaData() 0 11 2
A setCustomRepositoryClass() 0 4 1
A getSetters() 0 27 5
A getPropertyNameFromSetterName() 0 6 1
A getSingular() 0 28 3
A __construct() 0 3 1
A getStaticMethods() 0 10 2
A setChangeTrackingPolicy() 0 3 1
A getReflectionClass() 0 3 1
B getGetters() 0 33 7
A loadDoctrineMetaData() 0 24 4
A getPlural() 0 14 3
A getEmbeddableProperties() 0 23 4
A getReflectionHelper() 0 7 2
A getNamespaceHelper() 0 7 2
A getGetterForSetter() 0 19 4
A getRequiredRelationProperties() 0 24 5
B getTypesFromVarComment() 0 31 6
A getPropertyNameFromGetterName() 0 6 1
A getShortName() 0 5 1
A setMetaData() 0 5 1
A getMetaData() 0 3 1

How to fix   Complexity   

Complex Class

Complex classes like DoctrineStaticMeta often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use DoctrineStaticMeta, and based on these observations, apply Extract Interface, too.

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 36
    public function __construct(string $entityFqn)
83
    {
84 36
        $this->reflectionClass = new \ts\Reflection\ReflectionClass($entityFqn);
85 36
    }
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 6
    public function getReflectionClass(): \ts\Reflection\ReflectionClass
387
    {
388 6
        return $this->reflectionClass;
389
    }
390
391
    /**
392
     * Get an array of setters by name
393
     *
394
     * @return array|string[]
395
     */
396 1
    public function getSetters(): array
397
    {
398 1
        if (null !== $this->setters) {
399
            return $this->setters;
400
        }
401
        $skip            = [
402 1
            'addPropertyChangedListener'     => true,
403
            'setEntityCollectionAndNotify'   => true,
404
            'addToEntityCollectionAndNotify' => true,
405
            'setEntityAndNotify'             => true,
406
        ];
407 1
        $this->setters   = [];
408 1
        $reflectionClass = $this->getReflectionClass();
409 1
        foreach ($reflectionClass->getMethods(
410 1
            \ReflectionMethod::IS_PRIVATE | \ReflectionMethod::IS_PUBLIC
411
        ) as $method) {
412 1
            $methodName = $method->getName();
413 1
            if (isset($skip[$methodName])) {
414 1
                continue;
415
            }
416 1
            if (\ts\stringStartsWith($methodName, 'set')) {
417 1
                $this->setters[$this->getGetterForSetter($methodName)] = $methodName;
418 1
                continue;
419
            }
420
        }
421
422 1
        return $this->setters;
423
    }
424
425 1
    private function getGetterForSetter(string $setterName): string
426
    {
427 1
        $propertyName    = $this->getPropertyNameFromSetterName($setterName);
428 1
        $matchingGetters = [];
429 1
        foreach ($this->getGetters() as $getterName) {
430 1
            $getterPropertyName = $this->getPropertyNameFromGetterName($getterName);
431 1
            if (strtolower($getterPropertyName) === strtolower($propertyName)) {
432 1
                $matchingGetters[] = $getterName;
433
            }
434
        }
435 1
        if (count($matchingGetters) !== 1) {
436
            throw new \RuntimeException(
437
                'Found either less or more than one matching getter for ' .
438
                $propertyName . ': ' . print_r($matchingGetters, true)
439
                . "\n Current Entity: " . $this->getReflectionClass()->getName()
440
            );
441
        }
442
443 1
        return current($matchingGetters);
444
    }
445
446 14
    public function getPropertyNameFromSetterName(string $setterName): string
447
    {
448 14
        $propertyName = preg_replace('%^(set|add)(.+)%', '$2', $setterName);
449 14
        $propertyName = lcfirst($propertyName);
450
451 14
        return $propertyName;
452
    }
453
454
    /**
455
     * Get an array of getters by name
456
     * [];
457
     *
458
     * @return array|string[]
459
     */
460 2
    public function getGetters(): array
461
    {
462 2
        if (null !== $this->getters) {
463 1
            return $this->getters;
464
        }
465
        $skip = [
466 2
            'getEntityFqn'          => true,
467
            'getDoctrineStaticMeta' => true,
468
            'isValid'               => true,
469
        ];
470
471 2
        $this->getters   = [];
472 2
        $reflectionClass = $this->getReflectionClass();
473 2
        foreach ($reflectionClass->getMethods(\ReflectionMethod::IS_PUBLIC) as $method) {
474 2
            $methodName = $method->getName();
475 2
            if (isset($skip[$methodName])) {
476 2
                continue;
477
            }
478 2
            if (\ts\stringStartsWith($methodName, 'get')) {
479 2
                $this->getters[] = $methodName;
480 2
                continue;
481
            }
482 2
            if (\ts\stringStartsWith($methodName, 'is')) {
483 2
                $this->getters[] = $methodName;
484 2
                continue;
485
            }
486 2
            if (\ts\stringStartsWith($methodName, 'has')) {
487
                $this->getters[] = $methodName;
488
                continue;
489
            }
490
        }
491
492 2
        return $this->getters;
493
    }
494
495 14
    public function getPropertyNameFromGetterName(string $getterName): string
496
    {
497 14
        $propertyName = preg_replace('%^(get|is|has)(.+)%', '$2', $getterName);
498 14
        $propertyName = lcfirst($propertyName);
499
500 14
        return $propertyName;
501
    }
502
503
    /**
504
     * Get the short name (without fully qualified namespace) of the current Entity
505
     *
506
     * @return string
507
     */
508 1
    public function getShortName(): string
509
    {
510 1
        $reflectionClass = $this->getReflectionClass();
511
512 1
        return $reflectionClass->getShortName();
513
    }
514
515 1
    public function getMetaData(): ClassMetadata
516
    {
517 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...
518
    }
519
520 1
    public function setMetaData(ClassMetadata $metaData): self
521
    {
522 1
        $this->metaData = $metaData;
523
524 1
        return $this;
525
    }
526
}
527