Failed Conditions
Pull Request — master (#7799)
by Guilherme
10:54
created

AnnotationDriver   F

Complexity

Total Complexity 100

Size/Duplication

Total Lines 877
Duplicated Lines 0 %

Test Coverage

Coverage 85.87%

Importance

Changes 1
Bugs 1 Features 0
Metric Value
eloc 360
dl 0
loc 877
ccs 310
cts 361
cp 0.8587
rs 2
c 1
b 1
f 0
wmc 100

24 Methods

Rating   Name   Duplication   Size   Complexity  
C getAllClassNames() 0 63 12
A addPaths() 0 3 1
A setFileExtension() 0 3 1
A isTransient() 0 11 3
A getExcludePaths() 0 3 1
A getPaths() 0 3 1
A convertClassAnnotationsToEmbeddableClassMetadata() 0 9 1
A convertClassAnnotationsToMappedSuperClassMetadata() 0 19 2
A getReader() 0 3 1
B convertClassAnnotationsToEntityClassMetadata() 0 84 11
A getFileExtension() 0 3 1
A __construct() 0 6 2
A convertClassAnnotationsToClassMetadata() 0 31 4
A addExcludePaths() 0 3 1
B loadMetadataForClass() 0 56 7
A getCascade() 0 16 3
C convertPropertyAnnotationsToProperty() 0 123 14
B attachEntityListeners() 0 36 7
B attachPropertyOverrides() 0 85 11
A attachLifecycleCallbacks() 0 25 5
A getClassAnnotations() 0 13 3
A getPropertyAnnotations() 0 13 3
A getMethodAnnotations() 0 13 3
A getFetchMode() 0 9 2

How to fix   Complexity   

Complex Class

Complex classes like AnnotationDriver 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 AnnotationDriver, and based on these observations, apply Extract Interface, too.

1
<?php /** @noinspection ALL */
2
3
declare(strict_types=1);
4
5
namespace Doctrine\ORM\Mapping\Driver;
6
7
use Doctrine\Common\Annotations\AnnotationReader;
8
use Doctrine\Common\Annotations\Reader;
9
use Doctrine\ORM\Annotation;
10
use Doctrine\ORM\Cache\Exception\CacheException;
11
use Doctrine\ORM\Events;
12
use Doctrine\ORM\Mapping;
13
use Doctrine\ORM\Mapping\Builder;
14
use FilesystemIterator;
15
use RecursiveDirectoryIterator;
16
use RecursiveIteratorIterator;
17
use RecursiveRegexIterator;
18
use ReflectionClass;
19
use ReflectionException;
20
use ReflectionMethod;
21
use ReflectionProperty;
22
use RegexIterator;
23
use RuntimeException;
24
use UnexpectedValueException;
25
use function array_diff;
26
use function array_intersect;
27
use function array_map;
28
use function array_merge;
29
use function array_unique;
30
use function class_exists;
31
use function constant;
32
use function count;
33
use function defined;
34
use function get_class;
35
use function get_declared_classes;
36
use function in_array;
37
use function is_dir;
38
use function is_numeric;
39
use function preg_match;
40
use function preg_quote;
41
use function realpath;
42
use function sprintf;
43
use function str_replace;
44
use function strpos;
45
46
/**
47
 * The AnnotationDriver reads the mapping metadata from docblock annotations.
48
 */
49
class AnnotationDriver implements MappingDriver
50
{
51
    /** @var int[] */
52
    protected $entityAnnotationClasses = [
53
        Annotation\Entity::class           => 1,
54
        Annotation\MappedSuperclass::class => 2,
55
    ];
56
57
    /**
58
     * The AnnotationReader.
59
     *
60
     * @var AnnotationReader
61
     */
62
    protected $reader;
63
64
    /**
65
     * The paths where to look for mapping files.
66
     *
67
     * @var string[]
68
     */
69
    protected $paths = [];
70
71
    /**
72
     * The paths excluded from path where to look for mapping files.
73
     *
74
     * @var string[]
75
     */
76
    protected $excludePaths = [];
77
78
    /**
79
     * The file extension of mapping documents.
80
     *
81
     * @var string
82
     */
83
    protected $fileExtension = '.php';
84
85
    /**
86
     * Cache for AnnotationDriver#getAllClassNames().
87
     *
88
     * @var string[]|null
89
     */
90
    protected $classNames;
91
92
    /**
93
     * Initializes a new AnnotationDriver that uses the given AnnotationReader for reading
94
     * docblock annotations.
95
     *
96
     * @param Reader               $reader The AnnotationReader to use, duck-typed.
97
     * @param string|string[]|null $paths  One or multiple paths where mapping classes can be found.
98
     */
99 2295
    public function __construct(Reader $reader, $paths = null)
100
    {
101 2295
        $this->reader = $reader;
0 ignored issues
show
Documentation Bug introduced by
$reader is of type Doctrine\Common\Annotations\Reader, but the property $reader was declared to be of type Doctrine\Common\Annotations\AnnotationReader. Are you sure that you always receive this specific sub-class here, or does it make sense to add an instanceof check?

Our type inference engine has found a suspicous assignment of a value to a property. This check raises an issue when a value that can be of a given class or a super-class is assigned to a property that is type hinted more strictly.

Either this assignment is in error or an instanceof check should be added for that assignment.

class Alien {}

class Dalek extends Alien {}

class Plot
{
    /** @var  Dalek */
    public $villain;
}

$alien = new Alien();
$plot = new Plot();
if ($alien instanceof Dalek) {
    $plot->villain = $alien;
}
Loading history...
102
103 2295
        if ($paths) {
104 2209
            $this->addPaths((array) $paths);
105
        }
106 2295
    }
107
108
    /**
109
     * Appends lookup paths to metadata driver.
110
     *
111
     * @param string[] $paths
112
     */
113 2213
    public function addPaths(array $paths)
114
    {
115 2213
        $this->paths = array_unique(array_merge($this->paths, $paths));
116 2213
    }
117
118
    /**
119
     * Retrieves the defined metadata lookup paths.
120
     *
121
     * @return string[]
122
     */
123
    public function getPaths()
124
    {
125
        return $this->paths;
126
    }
127
128
    /**
129
     * Append exclude lookup paths to metadata driver.
130
     *
131
     * @param string[] $paths
132
     */
133
    public function addExcludePaths(array $paths)
134
    {
135
        $this->excludePaths = array_unique(array_merge($this->excludePaths, $paths));
136
    }
137
138
    /**
139
     * Retrieve the defined metadata lookup exclude paths.
140
     *
141
     * @return string[]
142
     */
143
    public function getExcludePaths()
144
    {
145
        return $this->excludePaths;
146
    }
147
148
    /**
149
     * Retrieve the current annotation reader
150
     *
151
     * @return Reader
152
     */
153 1
    public function getReader()
154
    {
155 1
        return $this->reader;
156
    }
157
158
    /**
159
     * Gets the file extension used to look for mapping files under.
160
     *
161
     * @return string
162
     */
163
    public function getFileExtension()
164
    {
165
        return $this->fileExtension;
166
    }
167
168
    /**
169
     * Sets the file extension used to look for mapping files under.
170
     *
171
     * @param string $fileExtension The file extension to set.
172
     */
173
    public function setFileExtension($fileExtension)
174
    {
175
        $this->fileExtension = $fileExtension;
176
    }
177
178
    /**
179
     * Returns whether the class with the specified name is transient. Only non-transient
180
     * classes, that is entities and mapped superclasses, should have their metadata loaded.
181
     *
182
     * A class is non-transient if it is annotated with an annotation
183
     * from the {@see AnnotationDriver::entityAnnotationClasses}.
184
     *
185
     * @param string $className
186
     *
187
     * @throws ReflectionException
188
     */
189 193
    public function isTransient($className) : bool
190
    {
191 193
        $classAnnotations = $this->reader->getClassAnnotations(new ReflectionClass($className));
192
193 193
        foreach ($classAnnotations as $annotation) {
194 188
            if (isset($this->entityAnnotationClasses[get_class($annotation)])) {
195 188
                return false;
196
            }
197
        }
198
199 12
        return true;
200
    }
201
202
    /**
203
     * {@inheritdoc}
204
     *
205
     * @throws ReflectionException
206
     */
207 60
    public function getAllClassNames() : array
208
    {
209 60
        if ($this->classNames !== null) {
210 45
            return $this->classNames;
211
        }
212
213 60
        if (! $this->paths) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $this->paths of type string[] is implicitly converted to a boolean; are you sure this is intended? If so, consider using empty($expr) instead to make it clear that you intend to check for an array without elements.

This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.

Consider making the comparison explicit by using empty(..) or ! empty(...) instead.

Loading history...
214
            throw Mapping\MappingException::pathRequired();
215
        }
216
217 60
        $classes       = [];
218 60
        $includedFiles = [];
219
220 60
        foreach ($this->paths as $path) {
221 60
            if (! is_dir($path)) {
222
                throw Mapping\MappingException::fileMappingDriversRequireConfiguredDirectoryPath($path);
223
            }
224
225 60
            $iterator = new RegexIterator(
226 60
                new RecursiveIteratorIterator(
227 60
                    new RecursiveDirectoryIterator($path, FilesystemIterator::SKIP_DOTS),
228 60
                    RecursiveIteratorIterator::LEAVES_ONLY
229
                ),
230 60
                '/^.+' . preg_quote($this->fileExtension) . '$/i',
231 60
                RecursiveRegexIterator::GET_MATCH
232
            );
233
234 60
            foreach ($iterator as $file) {
235 60
                $sourceFile = $file[0];
236
237 60
                if (! preg_match('(^phar:)i', $sourceFile)) {
238 60
                    $sourceFile = realpath($sourceFile);
239
                }
240
241 60
                foreach ($this->excludePaths as $excludePath) {
242
                    $exclude = str_replace('\\', '/', realpath($excludePath));
243
                    $current = str_replace('\\', '/', $sourceFile);
244
245
                    if (strpos($current, $exclude) !== false) {
246
                        continue 2;
247
                    }
248
                }
249
250 60
                require_once $sourceFile;
251
252 60
                $includedFiles[] = $sourceFile;
253
            }
254
        }
255
256 60
        $declared = get_declared_classes();
257
258 60
        foreach ($declared as $className) {
259 60
            $reflectionClass = new ReflectionClass($className);
260 60
            $sourceFile      = $reflectionClass->getFileName();
261
262 60
            if (in_array($sourceFile, $includedFiles, true) && ! $this->isTransient($className)) {
263 60
                $classes[] = $className;
264
            }
265
        }
266
267 60
        $this->classNames = $classes;
268
269 60
        return $classes;
270
    }
271
272
    /**
273
     * {@inheritDoc}
274
     *
275
     * @throws CacheException
276
     * @throws Mapping\MappingException
277
     * @throws ReflectionException
278
     * @throws RuntimeException
279
     * @throws UnexpectedValueException
280
     */
281 378
    public function loadMetadataForClass(
282
        string $className,
283
        ?Mapping\ComponentMetadata $parent,
284
        Mapping\ClassMetadataBuildingContext $metadataBuildingContext
285
    ) : Mapping\ComponentMetadata {
286 378
        $reflectionClass  = new ReflectionClass($className);
287 378
        $metadata         = new Mapping\ClassMetadata($className, $parent, $metadataBuildingContext);
288 378
        $classAnnotations = $this->getClassAnnotations($reflectionClass);
289 378
        $classMetadata    = $this->convertClassAnnotationsToClassMetadata(
290 378
            $classAnnotations,
291 378
            $reflectionClass,
292 378
            $metadata,
293 378
            $metadataBuildingContext
294
        );
295
296
        // Evaluate @Cache annotation
297 372
        if (isset($classAnnotations[Annotation\Cache::class])) {
298 18
            $cacheBuilder = new Builder\CacheMetadataBuilder($metadataBuildingContext);
299
300
            $cacheBuilder
301 18
                ->withComponentMetadata($metadata)
302 18
                ->withCacheAnnotation($classAnnotations[Annotation\Cache::class]);
303
304 18
            $metadata->setCache($cacheBuilder->build());
305
        }
306
307
        // Evaluate annotations on properties/fields
308
        /** @var ReflectionProperty $reflProperty */
309 372
        foreach ($reflectionClass->getProperties() as $reflectionProperty) {
310 372
            if ($reflectionProperty->getDeclaringClass()->getName() !== $reflectionClass->getName()) {
311 74
                continue;
312
            }
313
314 372
            $propertyAnnotations = $this->getPropertyAnnotations($reflectionProperty);
315 371
            $property            = $this->convertPropertyAnnotationsToProperty(
316 371
                $propertyAnnotations,
317 371
                $reflectionProperty,
318 371
                $classMetadata,
319 371
                $metadataBuildingContext
320
            );
321
322 370
            if ($classMetadata->isMappedSuperclass &&
323 370
                $property instanceof Mapping\ToManyAssociationMetadata &&
324 370
                ! $property->isOwningSide()) {
325 1
                throw Mapping\MappingException::illegalToManyAssociationOnMappedSuperclass(
326 1
                    $classMetadata->getClassName(),
327 1
                    $property->getName()
328
                );
329
            }
330
331 369
            $metadata->addProperty($property);
332
        }
333
334 369
        $this->attachPropertyOverrides($classAnnotations, $reflectionClass, $metadata, $metadataBuildingContext);
335
336 369
        return $classMetadata;
337
    }
338
339
    /**
340
     * @param Annotation\Annotation[] $classAnnotations
341
     *
342
     * @throws Mapping\MappingException
343
     * @throws UnexpectedValueException
344
     * @throws ReflectionException
345
     */
346 378
    private function convertClassAnnotationsToClassMetadata(
347
        array $classAnnotations,
348
        ReflectionClass $reflectionClass,
349
        Mapping\ClassMetadata $metadata,
350
        Mapping\ClassMetadataBuildingContext $metadataBuildingContext
351
    ) : Mapping\ClassMetadata {
352
        switch (true) {
353 378
            case isset($classAnnotations[Annotation\Entity::class]):
354 371
                return $this->convertClassAnnotationsToEntityClassMetadata(
355 371
                    $classAnnotations,
356 371
                    $reflectionClass,
357 371
                    $metadata,
358 371
                    $metadataBuildingContext
359
                );
360
361
                break;
0 ignored issues
show
Unused Code introduced by
break is not strictly necessary here and could be removed.

The break statement is not necessary if it is preceded for example by a return statement:

switch ($x) {
    case 1:
        return 'foo';
        break; // This break is not necessary and can be left off.
}

If you would like to keep this construct to be consistent with other case statements, you can safely mark this issue as a false-positive.

Loading history...
362
363 29
            case isset($classAnnotations[Annotation\MappedSuperclass::class]):
364 23
                return $this->convertClassAnnotationsToMappedSuperClassMetadata(
365 23
                    $classAnnotations,
366 23
                    $reflectionClass,
367 23
                    $metadata
368
                );
369 6
            case isset($classAnnotations[Annotation\Embeddable::class]):
370
                return $this->convertClassAnnotationsToEmbeddableClassMetadata(
371
                    $classAnnotations,
372
                    $reflectionClass,
373
                    $metadata
374
                );
375
            default:
376 6
                throw Mapping\MappingException::classIsNotAValidEntityOrMappedSuperClass($reflectionClass->getName());
377
        }
378
    }
379
380
    /**
381
     * @param Annotation\Annotation[] $classAnnotations
382
     *
383
     * @return Mapping\ClassMetadata
384
     *
385
     * @throws Mapping\MappingException
386
     * @throws ReflectionException
387
     * @throws UnexpectedValueException
388
     */
389 371
    private function convertClassAnnotationsToEntityClassMetadata(
390
        array $classAnnotations,
391
        ReflectionClass $reflectionClass,
392
        Mapping\ClassMetadata $metadata,
393
        Mapping\ClassMetadataBuildingContext $metadataBuildingContext
394
    ) {
395
        /** @var Annotation\Entity $entityAnnot */
396 371
        $entityAnnot = $classAnnotations[Annotation\Entity::class];
397
398 371
        if ($entityAnnot->repositoryClass !== null) {
399 3
            $metadata->setCustomRepositoryClassName($entityAnnot->repositoryClass);
400
        }
401
402 371
        if ($entityAnnot->readOnly) {
403 1
            $metadata->asReadOnly();
404
        }
405
406 371
        $metadata->isMappedSuperclass = false;
407 371
        $metadata->isEmbeddedClass    = false;
408
409
        // Process table information
410 371
        $parent = $metadata->getParent();
411
412 371
        if ($parent && $parent->inheritanceType === Mapping\InheritanceType::SINGLE_TABLE) {
413
            // Handle the case where a middle mapped super class inherits from a single table inheritance tree.
414
            do {
415 29
                if (! $parent->isMappedSuperclass) {
416 29
                    $metadata->setTable($parent->table);
417
418 29
                    break;
419
                }
420
421 4
                $parent = $parent->getParent();
422 29
            } while ($parent !== null);
423
        } else {
424 371
            $tableBuilder = new Builder\TableMetadataBuilder($metadataBuildingContext);
425
426
            $tableBuilder
427 371
                ->withEntityClassMetadata($metadata)
428 371
                ->withTableAnnotation($classAnnotations[Annotation\Table::class] ?? null);
429
430 371
            $metadata->setTable($tableBuilder->build());
431
        }
432
433
        // Evaluate @ChangeTrackingPolicy annotation
434 371
        if (isset($classAnnotations[Annotation\ChangeTrackingPolicy::class])) {
435 6
            $changeTrackingAnnot = $classAnnotations[Annotation\ChangeTrackingPolicy::class];
436
437 6
            $metadata->setChangeTrackingPolicy(
438 6
                constant(sprintf('%s::%s', Mapping\ChangeTrackingPolicy::class, $changeTrackingAnnot->value))
0 ignored issues
show
Bug introduced by
Accessing value on the interface Doctrine\ORM\Annotation\Annotation suggest that you code against a concrete implementation. How about adding an instanceof check?
Loading history...
439
            );
440
        }
441
442
        // Evaluate @InheritanceType annotation
443 371
        if (isset($classAnnotations[Annotation\InheritanceType::class])) {
444 80
            $inheritanceTypeAnnot = $classAnnotations[Annotation\InheritanceType::class];
445
446 80
            $metadata->setInheritanceType(
447 80
                constant(sprintf('%s::%s', Mapping\InheritanceType::class, $inheritanceTypeAnnot->value))
448
            );
449
450 80
            if ($metadata->inheritanceType !== Mapping\InheritanceType::NONE) {
451 80
                $discriminatorColumnBuilder = new Builder\DiscriminatorColumnMetadataBuilder($metadataBuildingContext);
452
453
                $discriminatorColumnBuilder
454 80
                    ->withComponentMetadata($metadata)
455 80
                    ->withDiscriminatorColumnAnnotation($classAnnotations[Annotation\DiscriminatorColumn::class] ?? null);
456
457 80
                $metadata->setDiscriminatorColumn($discriminatorColumnBuilder->build());
458
459
                // Evaluate DiscriminatorMap annotation
460 80
                if (isset($classAnnotations[Annotation\DiscriminatorMap::class])) {
461 77
                    $discriminatorMapAnnotation = $classAnnotations[Annotation\DiscriminatorMap::class];
462 77
                    $discriminatorMap           = $discriminatorMapAnnotation->value;
463
464 77
                    $metadata->setDiscriminatorMap($discriminatorMap);
465
                }
466
            }
467
        }
468
469 371
        $this->attachLifecycleCallbacks($classAnnotations, $reflectionClass, $metadata);
470 371
        $this->attachEntityListeners($classAnnotations, $metadata);
471
472 371
        return $metadata;
473
    }
474
475
    /**
476
     * @param Annotation\Annotation[] $classAnnotations
477
     *
478
     * @throws Mapping\MappingException
479
     * @throws ReflectionException
480
     */
481 23
    private function convertClassAnnotationsToMappedSuperClassMetadata(
482
        array $classAnnotations,
483
        ReflectionClass $reflectionClass,
484
        Mapping\ClassMetadata $metadata
485
    ) : Mapping\ClassMetadata {
486
        /** @var Annotation\MappedSuperclass $mappedSuperclassAnnot */
487 23
        $mappedSuperclassAnnot = $classAnnotations[Annotation\MappedSuperclass::class];
488
489 23
        if ($mappedSuperclassAnnot->repositoryClass !== null) {
490 2
            $metadata->setCustomRepositoryClassName($mappedSuperclassAnnot->repositoryClass);
491
        }
492
493 23
        $metadata->isMappedSuperclass = true;
494 23
        $metadata->isEmbeddedClass    = false;
495
496 23
        $this->attachLifecycleCallbacks($classAnnotations, $reflectionClass, $metadata);
497 23
        $this->attachEntityListeners($classAnnotations, $metadata);
498
499 23
        return $metadata;
500
    }
501
502
    /**
503
     * @param Annotation\Annotation[] $classAnnotations
504
     */
505
    private function convertClassAnnotationsToEmbeddableClassMetadata(
506
        array $classAnnotations,
0 ignored issues
show
Unused Code introduced by
The parameter $classAnnotations is not used and could be removed. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-unused  annotation

506
        /** @scrutinizer ignore-unused */ array $classAnnotations,

This check looks for parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
507
        ReflectionClass $reflectionClass,
0 ignored issues
show
Unused Code introduced by
The parameter $reflectionClass is not used and could be removed. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-unused  annotation

507
        /** @scrutinizer ignore-unused */ ReflectionClass $reflectionClass,

This check looks for parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
508
        Mapping\ClassMetadata $metadata
509
    ) : Mapping\ClassMetadata {
510
        $metadata->isMappedSuperclass = false;
511
        $metadata->isEmbeddedClass    = true;
512
513
        return $metadata;
514
    }
515
516
    /**
517
     * @param Annotation\Annotation[] $propertyAnnotations
518
     *
519
     * @todo guilhermeblanco Remove nullable typehint once embeddables are back
520
     */
521 371
    private function convertPropertyAnnotationsToProperty(
522
        array $propertyAnnotations,
523
        ReflectionProperty $reflectionProperty,
524
        Mapping\ClassMetadata $metadata,
525
        Mapping\ClassMetadataBuildingContext $metadataBuildingContext
526
    ) : ?Mapping\Property {
527
        switch (true) {
528 371
            case isset($propertyAnnotations[Annotation\Column::class]):
529 366
                $fieldBuilder  = new Builder\FieldMetadataBuilder($metadataBuildingContext);
530
                $fieldMetadata = $fieldBuilder
531 366
                    ->withComponentMetadata($metadata)
532 366
                    ->withFieldName($reflectionProperty->getName())
533 366
                    ->withColumnAnnotation($propertyAnnotations[Annotation\Column::class])
534 366
                    ->withIdAnnotation($propertyAnnotations[Annotation\Id::class] ?? null)
535 366
                    ->withVersionAnnotation($propertyAnnotations[Annotation\Version::class] ?? null)
536 366
                    ->withGeneratedValueAnnotation($propertyAnnotations[Annotation\GeneratedValue::class] ?? null)
537 366
                    ->withSequenceGeneratorAnnotation($propertyAnnotations[Annotation\SequenceGenerator::class] ?? null)
538 366
                    ->withCustomIdGeneratorAnnotation($propertyAnnotations[Annotation\CustomIdGenerator::class] ?? null)
539 366
                    ->build();
540
541
                // Prevent column duplication
542 366
                $columnName = $fieldMetadata->getColumnName();
543
544 366
                if ($metadata->checkPropertyDuplication($columnName)) {
0 ignored issues
show
Bug introduced by
It seems like $columnName can also be of type null; however, parameter $columnName of Doctrine\ORM\Mapping\Cla...ckPropertyDuplication() does only seem to accept string, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

544
                if ($metadata->checkPropertyDuplication(/** @scrutinizer ignore-type */ $columnName)) {
Loading history...
545
                    throw Mapping\MappingException::duplicateColumnName($metadata->getClassName(), $columnName);
546
                }
547
548 366
                $metadata->fieldNames[$fieldMetadata->getColumnName()] = $fieldMetadata->getName();
549
550 366
                return $fieldMetadata;
0 ignored issues
show
introduced by
Expected 0 lines after "return", found 1.
Loading history...
551
552 253
            case isset($propertyAnnotations[Annotation\OneToOne::class]):
553 110
                $oneToOneAssociationBuilder = new Builder\OneToOneAssociationMetadataBuilder($metadataBuildingContext);
554
                $associationMetadata        = $oneToOneAssociationBuilder
555 110
                    ->withComponentMetadata($metadata)
556 110
                    ->withFieldName($reflectionProperty->getName())
557 110
                    ->withOneToOneAnnotation($propertyAnnotations[Annotation\OneToOne::class] ?? null)
0 ignored issues
show
Bug introduced by
It seems like $propertyAnnotations[Doc...neToOne::class] ?? null can also be of type null; however, parameter $oneToOneAnnotation of Doctrine\ORM\Mapping\Bui...ithOneToOneAnnotation() does only seem to accept Doctrine\ORM\Annotation\OneToOne, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

557
                    ->withOneToOneAnnotation(/** @scrutinizer ignore-type */ $propertyAnnotations[Annotation\OneToOne::class] ?? null)
Loading history...
558 110
                    ->withIdAnnotation($propertyAnnotations[Annotation\Id::class] ?? null)
559 110
                    ->withCacheAnnotation($propertyAnnotations[Annotation\Cache::class] ?? null)
560 110
                    ->withJoinColumnsAnnotation($propertyAnnotations[Annotation\JoinColumns::class] ?? null)
561 110
                    ->withJoinColumnAnnotation($propertyAnnotations[Annotation\JoinColumn::class] ?? null)
562 110
                    ->build();
563
564
                // Prevent column duplication
565 110
                foreach ($associationMetadata->getJoinColumns() as $joinColumnMetadata) {
566 104
                    $columnName = $joinColumnMetadata->getColumnName();
567
568 104
                    if ($metadata->checkPropertyDuplication($columnName)) {
569
                        throw Mapping\MappingException::duplicateColumnName($metadata->getClassName(), $columnName);
570
                    }
571
572 104
                    if ($associationMetadata->isOwningSide()) {
573 104
                        $metadata->fieldNames[$columnName] = $associationMetadata->getName();
574
                    }
575
                }
576
577 110
                return $associationMetadata;
0 ignored issues
show
introduced by
Expected 0 lines after "return", found 1.
Loading history...
578
579 202
            case isset($propertyAnnotations[Annotation\ManyToOne::class]):
580 141
                $manyToOneAssociationBuilder = new Builder\ManyToOneAssociationMetadataBuilder($metadataBuildingContext);
581
                $associationMetadata         = $manyToOneAssociationBuilder
582 141
                    ->withComponentMetadata($metadata)
583 141
                    ->withFieldName($reflectionProperty->getName())
584 141
                    ->withManyToOneAnnotation($propertyAnnotations[Annotation\ManyToOne::class] ?? null)
0 ignored issues
show
Bug introduced by
It seems like $propertyAnnotations[Doc...nyToOne::class] ?? null can also be of type null; however, parameter $manyToOneAnnotation of Doctrine\ORM\Mapping\Bui...thManyToOneAnnotation() does only seem to accept Doctrine\ORM\Annotation\ManyToOne, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

584
                    ->withManyToOneAnnotation(/** @scrutinizer ignore-type */ $propertyAnnotations[Annotation\ManyToOne::class] ?? null)
Loading history...
585 141
                    ->withIdAnnotation($propertyAnnotations[Annotation\Id::class] ?? null)
586 141
                    ->withCacheAnnotation($propertyAnnotations[Annotation\Cache::class] ?? null)
587 141
                    ->withJoinColumnsAnnotation($propertyAnnotations[Annotation\JoinColumns::class] ?? null)
588 141
                    ->withJoinColumnAnnotation($propertyAnnotations[Annotation\JoinColumn::class] ?? null)
589 141
                    ->build();
590
591
                // Prevent column duplication
592 140
                foreach ($associationMetadata->getJoinColumns() as $joinColumnMetadata) {
593 140
                    $columnName = $joinColumnMetadata->getColumnName();
594
595 140
                    if ($metadata->checkPropertyDuplication($columnName)) {
596
                        throw Mapping\MappingException::duplicateColumnName($metadata->getClassName(), $columnName);
597
                    }
598
599 140
                    if ($associationMetadata->isOwningSide()) {
600 140
                        $metadata->fieldNames[$columnName] = $associationMetadata->getName();
601
                    }
602
                }
603
604 140
                return $associationMetadata;
0 ignored issues
show
introduced by
Expected 0 lines after "return", found 1.
Loading history...
605
606 163
            case isset($propertyAnnotations[Annotation\OneToMany::class]):
607 109
                $oneToManyAssociationBuilder = new Builder\OneToManyAssociationMetadataBuilder($metadataBuildingContext);
608
                $associationMetadata         = $oneToManyAssociationBuilder
0 ignored issues
show
introduced by
Useless variable $associationMetadata.
Loading history...
609 109
                    ->withComponentMetadata($metadata)
610 109
                    ->withFieldName($reflectionProperty->getName())
611 109
                    ->withOneToManyAnnotation($propertyAnnotations[Annotation\OneToMany::class] ?? null)
0 ignored issues
show
Bug introduced by
It seems like $propertyAnnotations[Doc...eToMany::class] ?? null can also be of type null; however, parameter $oneToManyAnnotation of Doctrine\ORM\Mapping\Bui...thOneToManyAnnotation() does only seem to accept Doctrine\ORM\Annotation\OneToMany, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

611
                    ->withOneToManyAnnotation(/** @scrutinizer ignore-type */ $propertyAnnotations[Annotation\OneToMany::class] ?? null)
Loading history...
612 109
                    ->withIdAnnotation($propertyAnnotations[Annotation\Id::class] ?? null)
613 109
                    ->withCacheAnnotation($propertyAnnotations[Annotation\Cache::class] ?? null)
614 109
                    ->withOrderByAnnotation($propertyAnnotations[Annotation\OrderBy::class] ?? null)
615 109
                    ->build();
616
617 109
                return $associationMetadata;
0 ignored issues
show
introduced by
Expected 0 lines after "return", found 1.
Loading history...
618
619 104
            case isset($propertyAnnotations[Annotation\ManyToMany::class]):
620 89
                $manyToManyAssociationBuilder = new Builder\ManyToManyAssociationMetadataBuilder($metadataBuildingContext);
621
                $associationMetadata          = $manyToManyAssociationBuilder
0 ignored issues
show
introduced by
Useless variable $associationMetadata.
Loading history...
622 89
                    ->withComponentMetadata($metadata)
623 89
                    ->withFieldName($reflectionProperty->getName())
624 89
                    ->withManyToManyAnnotation($propertyAnnotations[Annotation\ManyToMany::class] ?? null)
0 ignored issues
show
Bug introduced by
It seems like $propertyAnnotations[Doc...yToMany::class] ?? null can also be of type null; however, parameter $manyToManyAnnotation of Doctrine\ORM\Mapping\Bui...hManyToManyAnnotation() does only seem to accept Doctrine\ORM\Annotation\ManyToMany, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

624
                    ->withManyToManyAnnotation(/** @scrutinizer ignore-type */ $propertyAnnotations[Annotation\ManyToMany::class] ?? null)
Loading history...
625 89
                    ->withIdAnnotation($propertyAnnotations[Annotation\Id::class] ?? null)
626 89
                    ->withCacheAnnotation($propertyAnnotations[Annotation\Cache::class] ?? null)
627 89
                    ->withJoinTableAnnotation($propertyAnnotations[Annotation\JoinTable::class] ?? null)
628 89
                    ->withOrderByAnnotation($propertyAnnotations[Annotation\OrderBy::class] ?? null)
629 89
                    ->build();
630
631 89
                return $associationMetadata;
0 ignored issues
show
introduced by
Expected 0 lines after "return", found 1.
Loading history...
632
633 29
            case isset($propertyAnnotations[Annotation\Embedded::class]):
634
                return null;
0 ignored issues
show
introduced by
Expected 0 lines after "return", found 1.
Loading history...
635
636
            default:
637 29
                $transientBuilder  = new Builder\TransientMetadataBuilder($metadataBuildingContext);
638
                $transientMetadata = $transientBuilder
0 ignored issues
show
introduced by
Useless variable $transientMetadata.
Loading history...
639 29
                    ->withComponentMetadata($metadata)
640 29
                    ->withFieldName($reflectionProperty->getName())
641 29
                    ->build();
642
643 29
                return $transientMetadata;
644
        }
645
    }
646
647
    /**
648
     * @param Annotation\Annotation[] $classAnnotations
649
     */
650 372
    private function attachLifecycleCallbacks(
651
        array $classAnnotations,
652
        ReflectionClass $reflectionClass,
653
        Mapping\ClassMetadata $metadata
654
    ) : void {
655
        // Evaluate @HasLifecycleCallbacks annotation
656 372
        if (isset($classAnnotations[Annotation\HasLifecycleCallbacks::class])) {
657
            $eventMap = [
658 14
                Events::prePersist  => Annotation\PrePersist::class,
659 14
                Events::postPersist => Annotation\PostPersist::class,
660 14
                Events::preUpdate   => Annotation\PreUpdate::class,
661 14
                Events::postUpdate  => Annotation\PostUpdate::class,
662 14
                Events::preRemove   => Annotation\PreRemove::class,
663 14
                Events::postRemove  => Annotation\PostRemove::class,
664 14
                Events::postLoad    => Annotation\PostLoad::class,
665 14
                Events::preFlush    => Annotation\PreFlush::class,
666
            ];
667
668
            /** @var ReflectionMethod $reflectionMethod */
669 14
            foreach ($reflectionClass->getMethods(ReflectionMethod::IS_PUBLIC) as $reflectionMethod) {
670 13
                $annotations = $this->getMethodAnnotations($reflectionMethod);
671
672 13
                foreach ($eventMap as $eventName => $annotationClassName) {
673 13
                    if (isset($annotations[$annotationClassName])) {
674 12
                        $metadata->addLifecycleCallback($eventName, $reflectionMethod->getName());
675
                    }
676
                }
677
            }
678
        }
679 372
    }
680
681
    /**
682
     * @param Annotation\Annotation[] $classAnnotations
683
     *
684
     * @throws ReflectionException
685
     * @throws Mapping\MappingException
686
     */
687 372
    private function attachEntityListeners(
688
        array $classAnnotations,
689
        Mapping\ClassMetadata $metadata
690
    ) : void {
691
        // Evaluate @EntityListeners annotation
692 372
        if (isset($classAnnotations[Annotation\EntityListeners::class])) {
693
            /** @var Annotation\EntityListeners $entityListenersAnnot */
694 8
            $entityListenersAnnot = $classAnnotations[Annotation\EntityListeners::class];
695
            $eventMap             = [
696 8
                Events::prePersist  => Annotation\PrePersist::class,
697 8
                Events::postPersist => Annotation\PostPersist::class,
698 8
                Events::preUpdate   => Annotation\PreUpdate::class,
699 8
                Events::postUpdate  => Annotation\PostUpdate::class,
700 8
                Events::preRemove   => Annotation\PreRemove::class,
701 8
                Events::postRemove  => Annotation\PostRemove::class,
702 8
                Events::postLoad    => Annotation\PostLoad::class,
703 8
                Events::preFlush    => Annotation\PreFlush::class,
704
            ];
705
706 8
            foreach ($entityListenersAnnot->value as $listenerClassName) {
707 8
                if (! class_exists($listenerClassName)) {
708
                    throw Mapping\MappingException::entityListenerClassNotFound(
709
                        $listenerClassName,
710
                        $metadata->getClassName()
711
                    );
712
                }
713
714 8
                $listenerClass = new ReflectionClass($listenerClassName);
715
716
                /** @var ReflectionMethod $reflectionMethod */
717 8
                foreach ($listenerClass->getMethods(ReflectionMethod::IS_PUBLIC) as $reflectionMethod) {
718 8
                    $annotations = $this->getMethodAnnotations($reflectionMethod);
719
720 8
                    foreach ($eventMap as $eventName => $annotationClassName) {
721 8
                        if (isset($annotations[$annotationClassName])) {
722 6
                            $metadata->addEntityListener($eventName, $listenerClassName, $reflectionMethod->getName());
723
                        }
724
                    }
725
                }
726
            }
727
        }
728 372
    }
729
730
    /**
731
     * @param Annotation\Annotation[] $classAnnotations
732
     *
733
     * @throws Mapping\MappingException
734
     */
735 369
    private function attachPropertyOverrides(
736
        array $classAnnotations,
737
        ReflectionClass $reflectionClass,
0 ignored issues
show
Unused Code introduced by
The parameter $reflectionClass is not used and could be removed. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-unused  annotation

737
        /** @scrutinizer ignore-unused */ ReflectionClass $reflectionClass,

This check looks for parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
738
        Mapping\ClassMetadata $metadata,
739
        Mapping\ClassMetadataBuildingContext $metadataBuildingContext
740
    ) : void {
741
        // Evaluate AssociationOverrides annotation
742 369
        if (isset($classAnnotations[Annotation\AssociationOverrides::class])) {
743 5
            $associationOverridesAnnot = $classAnnotations[Annotation\AssociationOverrides::class];
744
745 5
            foreach ($associationOverridesAnnot->value as $associationOverride) {
0 ignored issues
show
Bug introduced by
Accessing value on the interface Doctrine\ORM\Annotation\Annotation suggest that you code against a concrete implementation. How about adding an instanceof check?
Loading history...
746 5
                $fieldName = $associationOverride->name;
747 5
                $property  = $metadata->getProperty($fieldName);
748
749 5
                if (! $property) {
750
                    throw Mapping\MappingException::invalidOverrideFieldName($metadata->getClassName(), $fieldName);
751
                }
752
753 5
                $existingClass = get_class($property);
754 5
                $override      = new $existingClass($fieldName);
755
756 5
                $override->setTargetEntity($property->getTargetEntity());
0 ignored issues
show
Bug introduced by
The method getTargetEntity() does not exist on Doctrine\ORM\Mapping\Property. It seems like you code against a sub-type of Doctrine\ORM\Mapping\Property such as Doctrine\ORM\Mapping\AssociationMetadata. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

756
                $override->setTargetEntity($property->/** @scrutinizer ignore-call */ getTargetEntity());
Loading history...
757
758
                // Check for JoinColumn/JoinColumns annotations
759 5
                if ($associationOverride->joinColumns) {
760 3
                    $joinColumnBuilder = new Builder\JoinColumnMetadataBuilder($metadataBuildingContext);
761
762
                    $joinColumnBuilder
763 3
                        ->withComponentMetadata($metadata)
764 3
                        ->withFieldName($fieldName);
765
766 3
                    $joinColumns = [];
0 ignored issues
show
Unused Code introduced by
The assignment to $joinColumns is dead and can be removed.
Loading history...
767
768 3
                    foreach ($associationOverride->joinColumns as $joinColumnAnnotation) {
769 3
                        $joinColumnBuilder->withJoinColumnAnnotation($joinColumnAnnotation);
770
771 3
                        $override->addJoinColumn($joinColumnBuilder->build());
772
                    }
773
                }
774
775
                // Check for JoinTable annotations
776 5
                if ($associationOverride->joinTable) {
777 2
                    $joinTableBuilder = new Builder\JoinTableMetadataBuilder($metadataBuildingContext);
778
779
                    $joinTableBuilder
780 2
                        ->withComponentMetadata($metadata)
781 2
                        ->withFieldName($fieldName)
782 2
                        ->withTargetEntity($property->getTargetEntity())
783 2
                        ->withJoinTableAnnotation($associationOverride->joinTable);
784
785 2
                    $override->setJoinTable($joinTableBuilder->build());
786
                }
787
788
                // Check for inversedBy
789 5
                if ($associationOverride->inversedBy) {
790 1
                    $override->setInversedBy($associationOverride->inversedBy);
791
                }
792
793
                // Check for fetch
794 5
                if ($associationOverride->fetch) {
795 1
                    $override->setFetchMode(
796 1
                        constant(Mapping\FetchMode::class . '::' . $associationOverride->fetch)
797
                    );
798
                }
799
800 5
                $metadata->setPropertyOverride($override);
801
            }
802
        }
803
804
        // Evaluate AttributeOverrides annotation
805 369
        if (isset($classAnnotations[Annotation\AttributeOverrides::class])) {
806 2
            $attributeOverridesAnnot = $classAnnotations[Annotation\AttributeOverrides::class];
807 2
            $fieldBuilder            = new Builder\FieldMetadataBuilder($metadataBuildingContext);
808
809
            $fieldBuilder
810 2
                ->withComponentMetadata($metadata)
811 2
                ->withIdAnnotation(null)
812 2
                ->withVersionAnnotation(null);
813
814 2
            foreach ($attributeOverridesAnnot->value as $attributeOverrideAnnotation) {
815
                $fieldBuilder
816 2
                    ->withFieldName($attributeOverrideAnnotation->name)
817 2
                    ->withColumnAnnotation($attributeOverrideAnnotation->column);
818
819 2
                $metadata->setPropertyOverride($fieldBuilder->build());
820
            }
821
        }
822 369
    }
823
824
    /**
825
     * Attempts to resolve the cascade modes.
826
     *
827
     * @param string   $className        The class name.
828
     * @param string   $fieldName        The field name.
829
     * @param string[] $originalCascades The original unprocessed field cascades.
830
     *
831
     * @return string[] The processed field cascades.
832
     *
833
     * @throws Mapping\MappingException If a cascade option is not valid.
834
     */
835
    private function getCascade(string $className, string $fieldName, array $originalCascades) : array
0 ignored issues
show
Unused Code introduced by
The method getCascade() is not used, and could be removed.

This check looks for private methods that have been defined, but are not used inside the class.

Loading history...
836
    {
837
        $cascadeTypes = ['remove', 'persist', 'refresh'];
838
        $cascades     = array_map('strtolower', $originalCascades);
839
840
        if (in_array('all', $cascades, true)) {
841
            $cascades = $cascadeTypes;
842
        }
843
844
        if (count($cascades) !== count(array_intersect($cascades, $cascadeTypes))) {
845
            $diffCascades = array_diff($cascades, array_intersect($cascades, $cascadeTypes));
846
847
            throw Mapping\MappingException::invalidCascadeOption($diffCascades, $className, $fieldName);
848
        }
849
850
        return $cascades;
851
    }
852
853
    /**
854
     * Attempts to resolve the fetch mode.
855
     *
856
     * @param string $className The class name.
857
     * @param string $fetchMode The fetch mode.
858
     *
859
     * @return string The fetch mode as defined in ClassMetadata.
860
     *
861
     * @throws Mapping\MappingException If the fetch mode is not valid.
862
     */
863
    private function getFetchMode($className, $fetchMode) : string
0 ignored issues
show
Unused Code introduced by
The method getFetchMode() is not used, and could be removed.

This check looks for private methods that have been defined, but are not used inside the class.

Loading history...
864
    {
865
        $fetchModeConstant = sprintf('%s::%s', Mapping\FetchMode::class, $fetchMode);
866
867
        if (! defined($fetchModeConstant)) {
868
            throw Mapping\MappingException::invalidFetchMode($className, $fetchMode);
869
        }
870
871
        return constant($fetchModeConstant);
872
    }
873
874
    /**
875
     * @return Annotation\Annotation[]
876
     */
877 378
    private function getClassAnnotations(ReflectionClass $reflectionClass) : array
878
    {
879 378
        $classAnnotations = $this->reader->getClassAnnotations($reflectionClass);
880
881 378
        foreach ($classAnnotations as $key => $annot) {
882 372
            if (! is_numeric($key)) {
883
                continue;
884
            }
885
886 372
            $classAnnotations[get_class($annot)] = $annot;
887
        }
888
889 378
        return $classAnnotations;
890
    }
891
892
    /**
893
     * @return Annotation\Annotation[]
894
     */
895 372
    private function getPropertyAnnotations(ReflectionProperty $reflectionProperty) : array
896
    {
897 372
        $propertyAnnotations = $this->reader->getPropertyAnnotations($reflectionProperty);
898
899 371
        foreach ($propertyAnnotations as $key => $annot) {
900 371
            if (! is_numeric($key)) {
901
                continue;
902
            }
903
904 371
            $propertyAnnotations[get_class($annot)] = $annot;
905
        }
906
907 371
        return $propertyAnnotations;
908
    }
909
910
    /**
911
     * @return Annotation\Annotation[]
912
     */
913 21
    private function getMethodAnnotations(ReflectionMethod $reflectionMethod) : array
914
    {
915 21
        $methodAnnotations = $this->reader->getMethodAnnotations($reflectionMethod);
916
917 21
        foreach ($methodAnnotations as $key => $annot) {
918 18
            if (! is_numeric($key)) {
919
                continue;
920
            }
921
922 18
            $methodAnnotations[get_class($annot)] = $annot;
923
        }
924
925 21
        return $methodAnnotations;
926
    }
927
}
928