Failed Conditions
Pull Request — master (#7799)
by Guilherme
09:17
created

convertClassAnnotationsToMappedSuperClassMetadata()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 19
Code Lines 8

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 9
CRAP Score 2

Importance

Changes 0
Metric Value
cc 2
eloc 8
nc 2
nop 3
dl 0
loc 19
ccs 9
cts 9
cp 1
crap 2
rs 10
c 0
b 0
f 0
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 2297
    public function __construct(Reader $reader, $paths = null)
100
    {
101 2297
        $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 2297
        if ($paths) {
104 2211
            $this->addPaths((array) $paths);
105
        }
106 2297
    }
107
108
    /**
109
     * Appends lookup paths to metadata driver.
110
     *
111
     * @param string[] $paths
112
     */
113 2215
    public function addPaths(array $paths)
114
    {
115 2215
        $this->paths = array_unique(array_merge($this->paths, $paths));
116 2215
    }
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 379
    public function loadMetadataForClass(
282
        string $className,
283
        ?Mapping\ComponentMetadata $parent,
284
        Mapping\ClassMetadataBuildingContext $metadataBuildingContext
285
    ) : Mapping\ComponentMetadata {
286 379
        $reflectionClass  = new ReflectionClass($className);
287 379
        $metadata         = new Mapping\ClassMetadata($className, $parent, $metadataBuildingContext);
288 379
        $classAnnotations = $this->getClassAnnotations($reflectionClass);
289 379
        $classMetadata    = $this->convertClassAnnotationsToClassMetadata(
290 379
            $classAnnotations,
291 379
            $reflectionClass,
292 379
            $metadata,
293 379
            $metadataBuildingContext
294
        );
295
296
        // Evaluate @Cache annotation
297 373
        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 373
        foreach ($reflectionClass->getProperties() as $reflectionProperty) {
310 373
            if ($reflectionProperty->getDeclaringClass()->getName() !== $reflectionClass->getName()) {
311 74
                continue;
312
            }
313
314 373
            $propertyAnnotations = $this->getPropertyAnnotations($reflectionProperty);
315 372
            $property            = $this->convertPropertyAnnotationsToProperty(
316 372
                $propertyAnnotations,
317 372
                $reflectionProperty,
318 372
                $classMetadata,
319 372
                $metadataBuildingContext
320
            );
321
322 371
            if ($classMetadata->isMappedSuperclass &&
323 371
                $property instanceof Mapping\ToManyAssociationMetadata &&
324 371
                ! $property->isOwningSide()) {
325 1
                throw Mapping\MappingException::illegalToManyAssociationOnMappedSuperclass(
326 1
                    $classMetadata->getClassName(),
327 1
                    $property->getName()
328
                );
329
            }
330
331 370
            $metadata->addProperty($property);
332
        }
333
334 370
        $this->attachPropertyOverrides($classAnnotations, $reflectionClass, $metadata, $metadataBuildingContext);
335
336 370
        return $classMetadata;
337
    }
338
339
    /**
340
     * @param Annotation\Annotation[] $classAnnotations
341
     *
342
     * @throws Mapping\MappingException
343
     * @throws UnexpectedValueException
344
     * @throws ReflectionException
345
     */
346 379
    private function convertClassAnnotationsToClassMetadata(
347
        array $classAnnotations,
348
        ReflectionClass $reflectionClass,
349
        Mapping\ClassMetadata $metadata,
350
        Mapping\ClassMetadataBuildingContext $metadataBuildingContext
351
    ) : Mapping\ClassMetadata {
352
        switch (true) {
353 379
            case isset($classAnnotations[Annotation\Entity::class]):
354 372
                return $this->convertClassAnnotationsToEntityClassMetadata(
355 372
                    $classAnnotations,
356 372
                    $reflectionClass,
357 372
                    $metadata,
358 372
                    $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 372
    private function convertClassAnnotationsToEntityClassMetadata(
390
        array $classAnnotations,
391
        ReflectionClass $reflectionClass,
392
        Mapping\ClassMetadata $metadata,
393
        Mapping\ClassMetadataBuildingContext $metadataBuildingContext
394
    ) {
395
        /** @var Annotation\Entity $entityAnnot */
396 372
        $entityAnnot = $classAnnotations[Annotation\Entity::class];
397
398 372
        if ($entityAnnot->repositoryClass !== null) {
399 3
            $metadata->setCustomRepositoryClassName($entityAnnot->repositoryClass);
400
        }
401
402 372
        if ($entityAnnot->readOnly) {
403 1
            $metadata->asReadOnly();
404
        }
405
406 372
        $metadata->isMappedSuperclass = false;
407 372
        $metadata->isEmbeddedClass    = false;
408
409
        // Process table information
410 372
        $parent = $metadata->getParent();
411
412 372
        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 372
            $tableBuilder = new Builder\TableMetadataBuilder($metadataBuildingContext);
425
426
            $tableBuilder
427 372
                ->withEntityClassMetadata($metadata)
428 372
                ->withTableAnnotation($classAnnotations[Annotation\Table::class] ?? null);
429
430 372
            $metadata->setTable($tableBuilder->build());
431
        }
432
433
        // Evaluate @ChangeTrackingPolicy annotation
434 372
        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 372
        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 372
        $this->attachLifecycleCallbacks($classAnnotations, $reflectionClass, $metadata);
470 372
        $this->attachEntityListeners($classAnnotations, $metadata);
471
472 372
        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 372
    private function convertPropertyAnnotationsToProperty(
522
        array $propertyAnnotations,
523
        ReflectionProperty $reflectionProperty,
524
        Mapping\ClassMetadata $metadata,
525
        Mapping\ClassMetadataBuildingContext $metadataBuildingContext
526
    ) : ?Mapping\Property {
527
        switch (true) {
528 372
            case isset($propertyAnnotations[Annotation\Column::class]):
529 367
                $fieldBuilder  = new Builder\FieldMetadataBuilder($metadataBuildingContext);
530
                $fieldMetadata = $fieldBuilder
531 367
                    ->withComponentMetadata($metadata)
532 367
                    ->withFieldName($reflectionProperty->getName())
533 367
                    ->withColumnAnnotation($propertyAnnotations[Annotation\Column::class])
534 367
                    ->withIdAnnotation($propertyAnnotations[Annotation\Id::class] ?? null)
535 367
                    ->withVersionAnnotation($propertyAnnotations[Annotation\Version::class] ?? null)
536 367
                    ->withGeneratedValueAnnotation($propertyAnnotations[Annotation\GeneratedValue::class] ?? null)
537 367
                    ->withSequenceGeneratorAnnotation($propertyAnnotations[Annotation\SequenceGenerator::class] ?? null)
538 367
                    ->withCustomIdGeneratorAnnotation($propertyAnnotations[Annotation\CustomIdGenerator::class] ?? null)
539 367
                    ->build();
540
541
                // Prevent column duplication
542 367
                $columnName = $fieldMetadata->getColumnName();
543
544 367
                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 367
                $metadata->fieldNames[$fieldMetadata->getColumnName()] = $fieldMetadata->getName();
549
550 367
                return $fieldMetadata;
0 ignored issues
show
introduced by
Expected 0 lines after "return", found 1.
Loading history...
551
552 254
            case isset($propertyAnnotations[Annotation\OneToOne::class]):
553 111
                $oneToOneAssociationBuilder = new Builder\OneToOneAssociationMetadataBuilder($metadataBuildingContext);
554
                $associationMetadata        = $oneToOneAssociationBuilder
555 111
                    ->withComponentMetadata($metadata)
556 111
                    ->withFieldName($reflectionProperty->getName())
557 111
                    ->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 111
                    ->withIdAnnotation($propertyAnnotations[Annotation\Id::class] ?? null)
559 111
                    ->withCacheAnnotation($propertyAnnotations[Annotation\Cache::class] ?? null)
560 111
                    ->withJoinColumnsAnnotation($propertyAnnotations[Annotation\JoinColumns::class] ?? null)
561 111
                    ->withJoinColumnAnnotation($propertyAnnotations[Annotation\JoinColumn::class] ?? null)
562 111
                    ->build();
563
564
                // Prevent column duplication
565 111
                foreach ($associationMetadata->getJoinColumns() as $joinColumnMetadata) {
566 105
                    $columnName = $joinColumnMetadata->getColumnName();
567
568
                    // @todo guilhermeblanco Open an issue to discuss making this scenario impossible.
569
                    //if ($metadata->checkPropertyDuplication($columnName)) {
570
                    //    throw Mapping\MappingException::duplicateColumnName($metadata->getClassName(), $columnName);
571
                    //}
572
573 105
                    if ($associationMetadata->isOwningSide()) {
574 105
                        $metadata->fieldNames[$columnName] = $associationMetadata->getName();
575
                    }
576
                }
577
578 111
                return $associationMetadata;
0 ignored issues
show
introduced by
Expected 0 lines after "return", found 1.
Loading history...
579
580 202
            case isset($propertyAnnotations[Annotation\ManyToOne::class]):
581 141
                $manyToOneAssociationBuilder = new Builder\ManyToOneAssociationMetadataBuilder($metadataBuildingContext);
582
                $associationMetadata         = $manyToOneAssociationBuilder
583 141
                    ->withComponentMetadata($metadata)
584 141
                    ->withFieldName($reflectionProperty->getName())
585 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

585
                    ->withManyToOneAnnotation(/** @scrutinizer ignore-type */ $propertyAnnotations[Annotation\ManyToOne::class] ?? null)
Loading history...
586 141
                    ->withIdAnnotation($propertyAnnotations[Annotation\Id::class] ?? null)
587 141
                    ->withCacheAnnotation($propertyAnnotations[Annotation\Cache::class] ?? null)
588 141
                    ->withJoinColumnsAnnotation($propertyAnnotations[Annotation\JoinColumns::class] ?? null)
589 141
                    ->withJoinColumnAnnotation($propertyAnnotations[Annotation\JoinColumn::class] ?? null)
590 141
                    ->build();
591
592
                // Prevent column duplication
593 140
                foreach ($associationMetadata->getJoinColumns() as $joinColumnMetadata) {
594 140
                    $columnName = $joinColumnMetadata->getColumnName();
595
596
                    // @todo guilhermeblanco Open an issue to discuss making this scenario impossible.
597
                    //if ($metadata->checkPropertyDuplication($columnName)) {
598
                    //    throw Mapping\MappingException::duplicateColumnName($metadata->getClassName(), $columnName);
599
                    //}
600
601 140
                    if ($associationMetadata->isOwningSide()) {
602 140
                        $metadata->fieldNames[$columnName] = $associationMetadata->getName();
603
                    }
604
                }
605
606 140
                return $associationMetadata;
0 ignored issues
show
introduced by
Expected 0 lines after "return", found 1.
Loading history...
607
608 163
            case isset($propertyAnnotations[Annotation\OneToMany::class]):
609 109
                $oneToManyAssociationBuilder = new Builder\OneToManyAssociationMetadataBuilder($metadataBuildingContext);
610
                $associationMetadata         = $oneToManyAssociationBuilder
0 ignored issues
show
introduced by
Useless variable $associationMetadata.
Loading history...
611 109
                    ->withComponentMetadata($metadata)
612 109
                    ->withFieldName($reflectionProperty->getName())
613 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

613
                    ->withOneToManyAnnotation(/** @scrutinizer ignore-type */ $propertyAnnotations[Annotation\OneToMany::class] ?? null)
Loading history...
614 109
                    ->withIdAnnotation($propertyAnnotations[Annotation\Id::class] ?? null)
615 109
                    ->withCacheAnnotation($propertyAnnotations[Annotation\Cache::class] ?? null)
616 109
                    ->withOrderByAnnotation($propertyAnnotations[Annotation\OrderBy::class] ?? null)
617 109
                    ->build();
618
619 109
                return $associationMetadata;
0 ignored issues
show
introduced by
Expected 0 lines after "return", found 1.
Loading history...
620
621 104
            case isset($propertyAnnotations[Annotation\ManyToMany::class]):
622 89
                $manyToManyAssociationBuilder = new Builder\ManyToManyAssociationMetadataBuilder($metadataBuildingContext);
623
                $associationMetadata          = $manyToManyAssociationBuilder
0 ignored issues
show
introduced by
Useless variable $associationMetadata.
Loading history...
624 89
                    ->withComponentMetadata($metadata)
625 89
                    ->withFieldName($reflectionProperty->getName())
626 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

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

739
        /** @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...
740
        Mapping\ClassMetadata $metadata,
741
        Mapping\ClassMetadataBuildingContext $metadataBuildingContext
742
    ) : void {
743
        // Evaluate AssociationOverrides annotation
744 370
        if (isset($classAnnotations[Annotation\AssociationOverrides::class])) {
745 5
            $associationOverridesAnnot = $classAnnotations[Annotation\AssociationOverrides::class];
746
747 5
            foreach ($associationOverridesAnnot->value as $associationOverrideAnnotation) {
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...
748 5
                $fieldName = $associationOverrideAnnotation->name;
749 5
                $property  = $metadata->getProperty($fieldName);
750
751 5
                if (! $property) {
752
                    throw Mapping\MappingException::invalidOverrideFieldName($metadata->getClassName(), $fieldName);
753
                }
754
755 5
                $override = clone $property;
756
757
                // Check for JoinColumn/JoinColumns annotations
758 5
                if ($associationOverrideAnnotation->joinColumns) {
759 3
                    $joinColumnBuilder = new Builder\JoinColumnMetadataBuilder($metadataBuildingContext);
760
761
                    $joinColumnBuilder
762 3
                        ->withComponentMetadata($metadata)
763 3
                        ->withFieldName($fieldName);
764
765 3
                    $joinColumns = [];
766
767 3
                    foreach ($associationOverrideAnnotation->joinColumns as $joinColumnAnnotation) {
768 3
                        $joinColumnBuilder->withJoinColumnAnnotation($joinColumnAnnotation);
769
770 3
                        $joinColumnMetadata = $joinColumnBuilder->build();
771 3
                        $columnName         = $joinColumnMetadata->getColumnName();
772
773
                        // @todo guilhermeblanco Open an issue to discuss making this scenario impossible.
774
                        //if ($metadata->checkPropertyDuplication($columnName)) {
775
                        //    throw Mapping\MappingException::duplicateColumnName($metadata->getClassName(), $columnName);
776
                        //}
777
778 3
                        if ($override->isOwningSide()) {
0 ignored issues
show
Bug introduced by
The method isOwningSide() 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

778
                        if ($override->/** @scrutinizer ignore-call */ isOwningSide()) {
Loading history...
779 3
                            $metadata->fieldNames[$columnName] = $fieldName;
780
                        }
781
782 3
                        $joinColumns[] = $joinColumnMetadata;
783
                    }
784
785 3
                    $override->setJoinColumns($joinColumns);
0 ignored issues
show
Bug introduced by
The method setJoinColumns() 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\ToOneAssociationMetadata. ( Ignorable by Annotation )

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

785
                    $override->/** @scrutinizer ignore-call */ 
786
                               setJoinColumns($joinColumns);
Loading history...
786
                }
787
788
                // Check for JoinTable annotations
789 5
                if ($associationOverrideAnnotation->joinTable) {
790 2
                    $joinTableBuilder = new Builder\JoinTableMetadataBuilder($metadataBuildingContext);
791
792
                    $joinTableBuilder
793 2
                        ->withComponentMetadata($metadata)
794 2
                        ->withFieldName($fieldName)
795 2
                        ->withTargetEntity($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

795
                        ->withTargetEntity($property->/** @scrutinizer ignore-call */ getTargetEntity())
Loading history...
796 2
                        ->withJoinTableAnnotation($associationOverrideAnnotation->joinTable);
797
798 2
                    $override->setJoinTable($joinTableBuilder->build());
0 ignored issues
show
Bug introduced by
The method setJoinTable() 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\ManyToManyAssociationMetadata. ( Ignorable by Annotation )

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

798
                    $override->/** @scrutinizer ignore-call */ 
799
                               setJoinTable($joinTableBuilder->build());
Loading history...
799
                }
800
801
                // Check for inversedBy
802 5
                if ($associationOverrideAnnotation->inversedBy) {
803 1
                    $override->setInversedBy($associationOverrideAnnotation->inversedBy);
0 ignored issues
show
Bug introduced by
The method setInversedBy() 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

803
                    $override->/** @scrutinizer ignore-call */ 
804
                               setInversedBy($associationOverrideAnnotation->inversedBy);
Loading history...
804
                }
805
806
                // Check for fetch
807 5
                if ($associationOverrideAnnotation->fetch) {
808 1
                    $override->setFetchMode(constant(Mapping\FetchMode::class . '::' . $associationOverrideAnnotation->fetch));
0 ignored issues
show
Bug introduced by
The method setFetchMode() 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

808
                    $override->/** @scrutinizer ignore-call */ 
809
                               setFetchMode(constant(Mapping\FetchMode::class . '::' . $associationOverrideAnnotation->fetch));
Loading history...
809
                }
810
811 5
                $metadata->setPropertyOverride($override);
812
            }
813
        }
814
815
        // Evaluate AttributeOverrides annotation
816 370
        if (isset($classAnnotations[Annotation\AttributeOverrides::class])) {
817 3
            $attributeOverridesAnnot = $classAnnotations[Annotation\AttributeOverrides::class];
818 3
            $fieldBuilder            = new Builder\FieldMetadataBuilder($metadataBuildingContext);
819
820
            $fieldBuilder
821 3
                ->withComponentMetadata($metadata)
822 3
                ->withIdAnnotation(null)
823 3
                ->withVersionAnnotation(null);
824
825 3
            foreach ($attributeOverridesAnnot->value as $attributeOverrideAnnotation) {
826 3
                $fieldName = $attributeOverrideAnnotation->name;
827 3
                $property  = $metadata->getProperty($fieldName);
828
829 3
                if (! $property) {
830
                    throw Mapping\MappingException::invalidOverrideFieldName($metadata->getClassName(), $fieldName);
831
                }
832
833
                $fieldBuilder
834 3
                    ->withFieldName($fieldName)
835 3
                    ->withColumnAnnotation($attributeOverrideAnnotation->column);
836
837 3
                $fieldMetadata = $fieldBuilder->build();
838 3
                $columnName    = $fieldMetadata->getColumnName();
839
840
                // Prevent column duplication
841 3
                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

841
                if ($metadata->checkPropertyDuplication(/** @scrutinizer ignore-type */ $columnName)) {
Loading history...
842
                    throw Mapping\MappingException::duplicateColumnName($metadata->getClassName(), $columnName);
843
                }
844
845 3
                $metadata->fieldNames[$fieldMetadata->getColumnName()] = $fieldName;
846
847 3
                $metadata->setPropertyOverride($fieldMetadata);
848
            }
849
        }
850 370
    }
851
852
    /**
853
     * Attempts to resolve the cascade modes.
854
     *
855
     * @param string   $className        The class name.
856
     * @param string   $fieldName        The field name.
857
     * @param string[] $originalCascades The original unprocessed field cascades.
858
     *
859
     * @return string[] The processed field cascades.
860
     *
861
     * @throws Mapping\MappingException If a cascade option is not valid.
862
     */
863
    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...
864
    {
865
        $cascadeTypes = ['remove', 'persist', 'refresh'];
866
        $cascades     = array_map('strtolower', $originalCascades);
867
868
        if (in_array('all', $cascades, true)) {
869
            $cascades = $cascadeTypes;
870
        }
871
872
        if (count($cascades) !== count(array_intersect($cascades, $cascadeTypes))) {
873
            $diffCascades = array_diff($cascades, array_intersect($cascades, $cascadeTypes));
874
875
            throw Mapping\MappingException::invalidCascadeOption($diffCascades, $className, $fieldName);
876
        }
877
878
        return $cascades;
879
    }
880
881
    /**
882
     * Attempts to resolve the fetch mode.
883
     *
884
     * @param string $className The class name.
885
     * @param string $fetchMode The fetch mode.
886
     *
887
     * @return string The fetch mode as defined in ClassMetadata.
888
     *
889
     * @throws Mapping\MappingException If the fetch mode is not valid.
890
     */
891
    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...
892
    {
893
        $fetchModeConstant = sprintf('%s::%s', Mapping\FetchMode::class, $fetchMode);
894
895
        if (! defined($fetchModeConstant)) {
896
            throw Mapping\MappingException::invalidFetchMode($className, $fetchMode);
897
        }
898
899
        return constant($fetchModeConstant);
900
    }
901
902
    /**
903
     * @return Annotation\Annotation[]
904
     */
905 379
    private function getClassAnnotations(ReflectionClass $reflectionClass) : array
906
    {
907 379
        $classAnnotations = $this->reader->getClassAnnotations($reflectionClass);
908
909 379
        foreach ($classAnnotations as $key => $annot) {
910 373
            if (! is_numeric($key)) {
911
                continue;
912
            }
913
914 373
            $classAnnotations[get_class($annot)] = $annot;
915
        }
916
917 379
        return $classAnnotations;
918
    }
919
920
    /**
921
     * @return Annotation\Annotation[]
922
     */
923 373
    private function getPropertyAnnotations(ReflectionProperty $reflectionProperty) : array
924
    {
925 373
        $propertyAnnotations = $this->reader->getPropertyAnnotations($reflectionProperty);
926
927 372
        foreach ($propertyAnnotations as $key => $annot) {
928 372
            if (! is_numeric($key)) {
929
                continue;
930
            }
931
932 372
            $propertyAnnotations[get_class($annot)] = $annot;
933
        }
934
935 372
        return $propertyAnnotations;
936
    }
937
938
    /**
939
     * @return Annotation\Annotation[]
940
     */
941 21
    private function getMethodAnnotations(ReflectionMethod $reflectionMethod) : array
942
    {
943 21
        $methodAnnotations = $this->reader->getMethodAnnotations($reflectionMethod);
944
945 21
        foreach ($methodAnnotations as $key => $annot) {
946 18
            if (! is_numeric($key)) {
947
                continue;
948
            }
949
950 18
            $methodAnnotations[get_class($annot)] = $annot;
951
        }
952
953 21
        return $methodAnnotations;
954
    }
955
}
956