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 (#152)
by joseph
17:58
created

DoctrineStaticMeta   C

Complexity

Total Complexity 57

Size/Duplication

Total Lines 492
Duplicated Lines 0 %

Test Coverage

Coverage 0%

Importance

Changes 0
Metric Value
eloc 193
dl 0
loc 492
ccs 0
cts 278
cp 0
rs 5.04
c 0
b 0
f 0
wmc 57

21 Methods

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