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
Push — master ( 0b4676...ecfcbd )
by joseph
17s queued 14s
created

DoctrineStaticMeta   F

Complexity

Total Complexity 62

Size/Duplication

Total Lines 515
Duplicated Lines 0 %

Test Coverage

Coverage 0%

Importance

Changes 2
Bugs 0 Features 0
Metric Value
eloc 205
c 2
b 0
f 0
dl 0
loc 515
ccs 0
cts 210
cp 0
rs 3.44
wmc 62

24 Methods

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

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
    public function __construct(string $entityFqn)
83
    {
84
        $this->reflectionClass = new \ts\Reflection\ReflectionClass($entityFqn);
85
    }
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
    public function getStaticMethods(): array
150
    {
151
        if (null !== $this->staticMethods) {
152
            return $this->staticMethods;
153
        }
154
        $this->staticMethods = $this->reflectionClass->getMethods(
155
            \ReflectionMethod::IS_STATIC
156
        );
157
158
        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
    public function getRequiredRelationProperties(): array
200
    {
201
        if (null !== $this->requiredRelationProperties) {
202
            return $this->requiredRelationProperties;
203
        }
204
        $traits = $this->reflectionClass->getTraits();
205
        $return = [];
206
        foreach ($traits as $traitName => $traitReflection) {
207
            if (false === \ts\stringContains($traitName, '\\HasRequired')) {
208
                continue;
209
            }
210
            if (false === \ts\stringContains($traitName, '\\Entity\\Relations\\')) {
211
                continue;
212
            }
213
214
            $property          = $traitReflection->getProperties()[0]->getName();
215
            $return[$property] = $this->getTypesFromVarComment(
216
                $property,
217
                $this->getReflectionHelper()->getTraitProvidingProperty($traitReflection, $property)
218
            );
219
        }
220
        $this->requiredRelationProperties = $return;
221
222
        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
    private function getTypesFromVarComment(string $property, ReflectionClass $traitReflection): array
236
    {
237
        $docComment = $this->reflectionClass->getProperty($property)->getDocComment();
238
        \preg_match('%@var\s*?(.+)%', $docComment, $matches);
239
        $traitCode = \ts\file_get_contents($traitReflection->getFileName());
240
        $types     = \explode('|', $matches[1]);
241
        $return    = [];
242
        foreach ($types as $type) {
243
            $type = \trim($type);
244
            if ('null' === $type) {
245
                continue;
246
            }
247
            if ('ArrayCollection' === $type) {
248
                continue;
249
            }
250
            $arrayNotation = '';
251
            if ('[]' === substr($type, -2)) {
252
                $type          = substr($type, 0, -2);
253
                $arrayNotation = '[]';
254
            }
255
            $pattern = "%^use (.+?)\\\\${type}(;| |\[)%m";
256
            \preg_match($pattern, $traitCode, $matches);
257
            if (!isset($matches[1])) {
258
                throw new \RuntimeException(
259
                    'Failed finding match for type ' . $type . ' in ' . $traitReflection->getFileName()
260
                );
261
            }
262
            $return[] = $matches[1] . '\\' . $type . $arrayNotation;
263
        }
264
265
        return $return;
266
    }
267
268
    private function getReflectionHelper(): ReflectionHelper
269
    {
270
        if (null === self::$reflectionHelper) {
271
            self::$reflectionHelper = new ReflectionHelper($this->getNamespaceHelper());
272
        }
273
274
        return self::$reflectionHelper;
275
    }
276
277
    private function getNamespaceHelper(): NamespaceHelper
278
    {
279
        if (null === self::$namespaceHelper) {
280
            self::$namespaceHelper = new NamespaceHelper();
281
        }
282
283
        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
    public function getEmbeddableProperties(): array
293
    {
294
        if (null !== $this->embeddableProperties) {
295
            return $this->embeddableProperties;
296
        }
297
        $traits = $this->reflectionClass->getTraits();
298
        $return = [];
299
        foreach ($traits as $traitName => $traitReflection) {
300
            if (\ts\stringContains($traitName, '\\Entity\\Embeddable\\Traits')) {
301
                $property                     = $traitReflection->getProperties()[0]->getName();
302
                $embeddableObjectInterfaceFqn = $this->getTypesFromVarComment(
303
                    $property,
304
                    $this->getReflectionHelper()->getTraitProvidingProperty($traitReflection, $property)
305
                )[0];
306
                $embeddableObject             = $this->getNamespaceHelper()
307
                                                     ->getEmbeddableObjectFqnFromEmbeddableObjectInterfaceFqn(
308
                                                         $embeddableObjectInterfaceFqn
309
                                                     );
310
                $return[$property]            = $embeddableObject;
311
            }
312
        }
313
314
        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
    public function getPlural(): string
327
    {
328
        try {
329
            if (null === $this->plural) {
330
                $singular     = $this->getSingular();
331
                $this->plural = Inflector::pluralize($singular);
332
            }
333
334
            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
    public function getSingular(): string
352
    {
353
        try {
354
            if (null === $this->singular) {
355
                $reflectionClass = $this->getReflectionClass();
356
357
                $shortName         = $reflectionClass->getShortName();
358
                $singularShortName = MappingHelper::singularize($shortName);
359
360
                $namespaceName   = $reflectionClass->getNamespaceName();
361
                $namespaceParts  = \explode(AbstractGenerator::ENTITIES_FOLDER_NAME, $namespaceName);
362
                $entityNamespace = \array_pop($namespaceParts);
363
364
                $namespacedShortName = \preg_replace(
365
                    '/\\\\/',
366
                    '',
367
                    $entityNamespace . $singularShortName
368
                );
369
370
                $this->singular = \lcfirst($namespacedShortName);
371
            }
372
373
            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
    public function getReflectionClass(): \ts\Reflection\ReflectionClass
387
    {
388
        return $this->reflectionClass;
389
    }
390
391
    public function getSetterNameFromPropertyName(string $property): ?string
392
    {
393
        foreach ($this->getSetters() as $setter) {
394
            if (preg_match('%^(set|add)' . $property . '%i', $setter)) {
395
                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
    public function getSetters(): array
408
    {
409
        if (null !== $this->setters) {
410
            return $this->setters;
411
        }
412
        $skip            = [
413
            'addPropertyChangedListener'     => true,
414
            'setEntityCollectionAndNotify'   => true,
415
            'addToEntityCollectionAndNotify' => true,
416
            'setEntityAndNotify'             => true,
417
        ];
418
        $this->setters   = [];
419
        $reflectionClass = $this->getReflectionClass();
420
        foreach ($reflectionClass->getMethods(
421
            \ReflectionMethod::IS_PRIVATE | \ReflectionMethod::IS_PUBLIC
422
        ) as $method) {
423
            $methodName = $method->getName();
424
            if (isset($skip[$methodName])) {
425
                continue;
426
            }
427
            if (\ts\stringStartsWith($methodName, 'set')) {
428
                $this->setters[$this->getGetterForSetter($methodName)] = $methodName;
429
                continue;
430
            }
431
        }
432
433
        return $this->setters;
434
    }
435
436
    private function getGetterForSetter(string $setterName): string
437
    {
438
        $propertyName    = $this->getPropertyNameFromSetterName($setterName);
439
        $matchingGetters = [];
440
        foreach ($this->getGetters() as $getterName) {
441
            $getterPropertyName = $this->getPropertyNameFromGetterName($getterName);
442
            if (strtolower($getterPropertyName) === strtolower($propertyName)) {
443
                $matchingGetters[] = $getterName;
444
            }
445
        }
446
        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
        return current($matchingGetters);
455
    }
456
457
    public function getPropertyNameFromSetterName(string $setterName): string
458
    {
459
        $propertyName = preg_replace('%^(set|add)(.+)%', '$2', $setterName);
460
        $propertyName = lcfirst($propertyName);
461
462
        return $propertyName;
463
    }
464
465
    /**
466
     * Get an array of getters by name
467
     *
468
     * @return array|string[]
469
     * @throws \ReflectionException
470
     */
471
    public function getGetters(): array
472
    {
473
        if (null !== $this->getters) {
474
            return $this->getters;
475
        }
476
        $skip = [
477
            'getEntityFqn'          => true,
478
            'getDoctrineStaticMeta' => true,
479
            'isValid'               => true,
480
            'getValidator'          => true,
481
        ];
482
483
        $this->getters   = [];
484
        $reflectionClass = $this->getReflectionClass();
485
        foreach ($reflectionClass->getMethods(\ReflectionMethod::IS_PUBLIC) as $method) {
486
            $methodName = $method->getName();
487
            if (isset($skip[$methodName])) {
488
                continue;
489
            }
490
            if (\ts\stringStartsWith($methodName, 'get')) {
491
                $this->getters[] = $methodName;
492
                continue;
493
            }
494
            if (\ts\stringStartsWith($methodName, 'is')) {
495
                $this->getters[] = $methodName;
496
                continue;
497
            }
498
            if (\ts\stringStartsWith($methodName, 'has')) {
499
                $this->getters[] = $methodName;
500
                continue;
501
            }
502
        }
503
504
        return $this->getters;
505
    }
506
507
    public function getPropertyNameFromGetterName(string $getterName): string
508
    {
509
        $propertyName = preg_replace('%^(get|is|has)(.+)%', '$2', $getterName);
510
        $propertyName = lcfirst($propertyName);
511
512
        return $propertyName;
513
    }
514
515
    /**
516
     * Get the short name (without fully qualified namespace) of the current Entity
517
     *
518
     * @return string
519
     */
520
    public function getShortName(): string
521
    {
522
        $reflectionClass = $this->getReflectionClass();
523
524
        return $reflectionClass->getShortName();
525
    }
526
527
    public function getMetaData(): ClassMetadata
528
    {
529
        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
    public function setMetaData(ClassMetadata $metaData): self
533
    {
534
        $this->metaData = $metaData;
535
536
        return $this;
537
    }
538
}
539