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 (#225)
by joseph
39:33
created

DoctrineStaticMeta::setTableName()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 4
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 2

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 1
eloc 2
nc 1
nop 1
dl 0
loc 4
ccs 0
cts 3
cp 0
crap 2
rs 10
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
    public function __construct(string $entityFqn)
93
    {
94
        $this->reflectionClass = new ReflectionClass($entityFqn);
95
        $this->runDsmInitMethods();
96
    }
97
98
    private function runDsmInitMethods(): void
99
    {
100
        $methodName = '__no_method__';
101
        try {
102
            $staticMethods = $this->getStaticMethods();
103
            //now loop through and call them
104
            foreach ($staticMethods as $method) {
105
                $methodName = $method->getName();
106
                if (
107
                    \ts\stringStartsWith($methodName, self::DSM_INIT_METHOD_PREFIX)
108
                ) {
109
                    $method->setAccessible(true);
110
                    $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
    }
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
    public function getStaticMethods(): array
134
    {
135
        if (null !== $this->staticMethods) {
136
            return $this->staticMethods;
137
        }
138
        $this->staticMethods = $this->reflectionClass->getMethods(
139
            \ReflectionMethod::IS_STATIC
140
        );
141
142
        return $this->staticMethods;
143
    }
144
145
    public function setRequiredRelationProperty(RequiredRelation $requiredRelation): self
146
    {
147
        $this->requiredRelationProperties[$requiredRelation->getPropertyName()] = $requiredRelation;
148
149
        return $this;
150
    }
151
152
    /**
153
     * @return RequiredRelation[]
154
     */
155
    public function getRequiredRelationProperties(): array
156
    {
157
        return $this->requiredRelationProperties ?? [];
158
    }
159
160
    public function getMetaData(): ClassMetadata
161
    {
162
        if ($this->metaData instanceof ClassMetadata) {
163
            return $this->metaData;
164
        }
165
        $this->metaData = new ClassMetadata($this->reflectionClass->getName());
166
        $this->buildMetaData();
167
168
        return $this->metaData;
169
    }
170
171
    public function setMetaData(ClassMetadata $metaData): self
172
    {
173
        $this->metaData = $metaData;
174
175
        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
    public function getEmbeddableProperties(): array
270
    {
271
        if (null !== $this->embeddableProperties) {
272
            return $this->embeddableProperties;
273
        }
274
        $traits = $this->reflectionClass->getTraits();
275
        $return = [];
276
        foreach ($traits as $traitName => $traitReflection) {
277
            if (\ts\stringContains($traitName, '\\Entity\\Embeddable\\Traits')) {
278
                $property                     = $traitReflection->getProperties()[0]->getName();
279
                $embeddableObjectInterfaceFqn = $this->getTypesFromVarComment(
280
                    $property,
281
                    $this->getReflectionHelper()->getTraitProvidingProperty($traitReflection, $property)->getFileName()
282
                )[0];
283
                $embeddableObject             = $this->getNamespaceHelper()
284
                                                     ->getEmbeddableObjectFqnFromEmbeddableObjectInterfaceFqn(
285
                                                         $embeddableObjectInterfaceFqn
286
                                                     );
287
                $return[$property]            = $embeddableObject;
288
            }
289
        }
290
291
        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
    private function getTypesFromVarComment(string $property, string $filename): array
305
    {
306
        $docComment = $this->reflectionClass->getProperty($property)->getDocComment();
307
        preg_match('%@var\s*?(.+)%', $docComment, $matches);
308
        $traitCode = \ts\file_get_contents($filename);
309
        $types     = explode('|', $matches[1]);
310
        $return    = [];
311
        foreach ($types as $type) {
312
            $type = trim($type);
313
            if ('null' === $type) {
314
                continue;
315
            }
316
            if ('ArrayCollection' === $type) {
317
                continue;
318
            }
319
            $arrayNotation = '';
320
            if ('[]' === substr($type, -2)) {
321
                $type          = substr($type, 0, -2);
322
                $arrayNotation = '[]';
323
            }
324
            $pattern = "%^use (.+?)\\\\${type}(;| |\[)%m";
325
            preg_match($pattern, $traitCode, $matches);
326
            if (!isset($matches[1])) {
327
                throw new RuntimeException(
328
                    'Failed finding match for type ' . $type . ' in ' . $filename
329
                );
330
            }
331
            $return[] = $matches[1] . '\\' . $type . $arrayNotation;
332
        }
333
334
        return $return;
335
    }
336
337
    private function getReflectionHelper(): ReflectionHelper
338
    {
339
        if (null === self::$reflectionHelper) {
340
            self::$reflectionHelper = new ReflectionHelper($this->getNamespaceHelper());
341
        }
342
343
        return self::$reflectionHelper;
344
    }
345
346
    private function getNamespaceHelper(): NamespaceHelper
347
    {
348
        if (null === self::$namespaceHelper) {
349
            self::$namespaceHelper = new NamespaceHelper();
350
        }
351
352
        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
    public function getPlural(): string
365
    {
366
        try {
367
            if (null === $this->plural) {
368
                $singular     = $this->getSingular();
369
                $this->plural = Inflector::pluralize($singular);
370
            }
371
372
            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
    public function getSingular(): string
390
    {
391
        try {
392
            if (null === $this->singular) {
393
                $reflectionClass = $this->getReflectionClass();
394
395
                $shortName         = $reflectionClass->getShortName();
396
                $singularShortName = MappingHelper::singularize($shortName);
397
398
                $namespaceName   = $reflectionClass->getNamespaceName();
399
                $namespaceParts  = explode(AbstractGenerator::ENTITIES_FOLDER_NAME, $namespaceName);
400
                $entityNamespace = array_pop($namespaceParts);
401
402
                $namespacedShortName = preg_replace(
403
                    '/\\\\/',
404
                    '',
405
                    $entityNamespace . $singularShortName
406
                );
407
408
                $this->singular = lcfirst($namespacedShortName);
409
            }
410
411
            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
    public function getReflectionClass(): ReflectionClass
425
    {
426
        return $this->reflectionClass;
427
    }
428
429
    public function getSetterNameFromPropertyName(string $property): ?string
430
    {
431
        foreach ($this->getSetters() as $setter) {
432
            if (preg_match('%^(set|add)' . $property . '%i', $setter)) {
433
                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
    public function getSetters(): array
447
    {
448
        if (null !== $this->setters) {
449
            return $this->setters;
450
        }
451
        $skip            = [
452
            'addPropertyChangedListener'     => true,
453
            'setEntityCollectionAndNotify'   => true,
454
            'addToEntityCollectionAndNotify' => true,
455
            'setEntityAndNotify'             => true,
456
        ];
457
        $this->setters   = [];
458
        $reflectionClass = $this->getReflectionClass();
459
        foreach (
460
            $reflectionClass->getMethods(
461
                \ReflectionMethod::IS_PRIVATE | \ReflectionMethod::IS_PUBLIC
462
            ) as $method
463
        ) {
464
            $methodName = $method->getName();
465
            if (isset($skip[$methodName])) {
466
                continue;
467
            }
468
            if (\ts\stringStartsWith($methodName, 'set')) {
469
                $this->setters[$this->getGetterForSetter($methodName)] = $methodName;
470
                continue;
471
            }
472
        }
473
474
        return $this->setters;
475
    }
476
477
    private function getGetterForSetter(string $setterName): string
478
    {
479
        $propertyName    = $this->getPropertyNameFromSetterName($setterName);
480
        $matchingGetters = [];
481
        foreach ($this->getGetters() as $getterName) {
482
            $getterPropertyName = $this->getPropertyNameFromGetterName($getterName);
483
            if (strtolower($getterPropertyName) === strtolower($propertyName)) {
484
                $matchingGetters[] = $getterName;
485
            }
486
        }
487
        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
        return current($matchingGetters);
496
    }
497
498
    public function getPropertyNameFromSetterName(string $setterName): string
499
    {
500
        $propertyName = preg_replace('%^(set|add)(.+)%', '$2', $setterName);
501
        $propertyName = lcfirst($propertyName);
502
503
        return $propertyName;
504
    }
505
506
    /**
507
     * Get an array of getters by name
508
     *
509
     * @return array|string[]
510
     * @throws ReflectionException
511
     */
512
    public function getGetters(): array
513
    {
514
        if (null !== $this->getters) {
515
            return $this->getters;
516
        }
517
        $skip = [
518
            'getEntityFqn'          => true,
519
            'getDoctrineStaticMeta' => true,
520
            'isValid'               => true,
521
            'getValidator'          => true,
522
        ];
523
524
        $this->getters   = [];
525
        $reflectionClass = $this->getReflectionClass();
526
        foreach ($reflectionClass->getMethods(\ReflectionMethod::IS_PUBLIC) as $method) {
527
            $methodName = $method->getName();
528
            if (isset($skip[$methodName])) {
529
                continue;
530
            }
531
            if (\ts\stringStartsWith($methodName, 'get')) {
532
                $this->getters[] = $methodName;
533
                continue;
534
            }
535
            if (\ts\stringStartsWith($methodName, 'is')) {
536
                $this->getters[] = $methodName;
537
                continue;
538
            }
539
            if (\ts\stringStartsWith($methodName, 'has')) {
540
                $this->getters[] = $methodName;
541
                continue;
542
            }
543
        }
544
545
        return $this->getters;
546
    }
547
548
    public function getPropertyNameFromGetterName(string $getterName): string
549
    {
550
        $propertyName = preg_replace('%^(get|is|has)(.+)%', '$2', $getterName);
551
        $propertyName = lcfirst($propertyName);
552
553
        return $propertyName;
554
    }
555
556
    /**
557
     * Get the short name (without fully qualified namespace) of the current Entity
558
     *
559
     * @return string
560
     */
561
    public function getShortName(): string
562
    {
563
        $reflectionClass = $this->getReflectionClass();
564
565
        return $reflectionClass->getShortName();
566
    }
567
}
568