Passed
Pull Request — master (#7024)
by Jefersson
13:23
created

etadata()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 21
Code Lines 9

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 6

Importance

Changes 0
Metric Value
cc 2
eloc 9
nc 2
nop 4
dl 0
loc 21
ccs 0
cts 16
cp 0
crap 6
rs 9.3142
c 0
b 0
f 0
1
<?php
2
3
declare(strict_types=1);
4
5
namespace Doctrine\ORM\Mapping\Driver\Annotation;
6
7
use Doctrine\Common\Annotations\AnnotationReader;
8
use Doctrine\DBAL\Types\Type;
9
use Doctrine\ORM\Annotation;
10
use Doctrine\ORM\Events;
11
use Doctrine\ORM\Mapping;
12
13
/**
14
 * The AnnotationDriver reads the mapping metadata from docblock annotations.
15
 */
16
class AnnotationDriver implements Mapping\Driver\MappingDriver
17
{
18
    /**
19
     * @var int[]
20
     */
21
    protected $entityAnnotationClasses = [
22
        Annotation\Entity::class           => 1,
23
        Annotation\MappedSuperclass::class => 2,
24
        Annotation\Embeddable::class       => 3,
25
    ];
26
27
    /**
28
     * The AnnotationReader.
29
     *
30
     * @var AnnotationReader
31
     */
32
    protected $reader;
33
34
    /**
35
     * The paths where to look for mapping files.
36
     *
37
     * @var string[]
38
     */
39
    protected $paths = [];
40
41
    /**
42
     * The paths excluded from path where to look for mapping files.
43
     *
44
     * @var string[]
45
     */
46
    protected $excludePaths = [];
47
48
    /**
49
     * The file extension of mapping documents.
50
     *
51
     * @var string
52
     */
53
    protected $fileExtension = '.php';
54
55
    /**
56
     * Cache for AnnotationDriver#getAllClassNames().
57
     *
58
     * @var string[]|null
59
     */
60
    protected $classNames;
61
62
    /**
63
     * Initializes a new AnnotationDriver that uses the given AnnotationReader for reading
64
     * docblock annotations.
65
     *
66
     * @param AnnotationReader     $reader The AnnotationReader to use, duck-typed.
67
     * @param string|string[]|null $paths  One or multiple paths where mapping classes can be found.
68
     */
69
    public function __construct($reader, $paths = null)
70
    {
71
        $this->reader = $reader;
72
73
        if ($paths) {
74
            $this->addPaths((array) $paths);
75
        }
76
    }
77
78
    /**
79
     * Appends lookup paths to metadata driver.
80
     *
81
     * @param string[] $paths
82
     */
83
    public function addPaths(array $paths)
84
    {
85
        $this->paths = array_unique(array_merge($this->paths, $paths));
86
    }
87
88
    /**
89
     * Retrieves the defined metadata lookup paths.
90
     *
91
     * @return string[]
92
     */
93
    public function getPaths()
94
    {
95
        return $this->paths;
96
    }
97
98
    /**
99
     * Append exclude lookup paths to metadata driver.
100
     *
101
     * @param string[] $paths
102
     */
103
    public function addExcludePaths(array $paths)
104
    {
105
        $this->excludePaths = array_unique(array_merge($this->excludePaths, $paths));
106
    }
107
108
    /**
109
     * Retrieve the defined metadata lookup exclude paths.
110
     *
111
     * @return string[]
112
     */
113
    public function getExcludePaths()
114
    {
115
        return $this->excludePaths;
116
    }
117
118
    /**
119
     * Retrieve the current annotation reader
120
     *
121
     * @return AnnotationReader
122
     */
123
    public function getReader()
124
    {
125
        return $this->reader;
126
    }
127
128
    /**
129
     * Gets the file extension used to look for mapping files under.
130
     *
131
     * @return string
132
     */
133
    public function getFileExtension()
134
    {
135
        return $this->fileExtension;
136
    }
137
138
    /**
139
     * Sets the file extension used to look for mapping files under.
140
     *
141
     * @param string $fileExtension The file extension to set.
142
     */
143
    public function setFileExtension($fileExtension)
144
    {
145
        $this->fileExtension = $fileExtension;
146
    }
147
148
    /**
149
     * Returns whether the class with the specified name is transient. Only non-transient
150
     * classes, that is entities and mapped superclasses, should have their metadata loaded.
151
     *
152
     * A class is non-transient if it is annotated with an annotation
153
     * from the {@see AnnotationDriver::entityAnnotationClasses}.
154
     *
155
     * @param string $className
156
     *
157
     * @return bool
158
     */
159
    public function isTransient($className)
160
    {
161
        $classAnnotations = $this->reader->getClassAnnotations(new \ReflectionClass($className));
162
163
        foreach ($classAnnotations as $annot) {
164
            if (isset($this->entityAnnotationClasses[get_class($annot)])) {
165
                return false;
166
            }
167
        }
168
        return true;
169
    }
170
171
    /**
172
     * {@inheritDoc}
173
     */
174
    public function getAllClassNames()
175
    {
176
        if ($this->classNames !== null) {
177
            return $this->classNames;
178
        }
179
180
        if (! $this->paths) {
181
            throw Mapping\MappingException::pathRequired();
182
        }
183
184
        $classes       = [];
185
        $includedFiles = [];
186
187
        foreach ($this->paths as $path) {
188
            if (! is_dir($path)) {
189
                throw Mapping\MappingException::fileMappingDriversRequireConfiguredDirectoryPath($path);
190
            }
191
192
            $iterator = new \RegexIterator(
193
                new \RecursiveIteratorIterator(
194
                    new \RecursiveDirectoryIterator($path, \FilesystemIterator::SKIP_DOTS),
195
                    \RecursiveIteratorIterator::LEAVES_ONLY
196
                ),
197
                '/^.+' . preg_quote($this->fileExtension) . '$/i',
198
                \RecursiveRegexIterator::GET_MATCH
199
            );
200
201
            foreach ($iterator as $file) {
202
                $sourceFile = $file[0];
203
204
                if (! preg_match('(^phar:)i', $sourceFile)) {
205
                    $sourceFile = realpath($sourceFile);
206
                }
207
208
                foreach ($this->excludePaths as $excludePath) {
209
                    $exclude = str_replace('\\', '/', realpath($excludePath));
210
                    $current = str_replace('\\', '/', $sourceFile);
211
212
                    if (strpos($current, $exclude) !== false) {
213
                        continue 2;
214
                    }
215
                }
216
217
                require_once $sourceFile;
218
219
                $includedFiles[] = $sourceFile;
220
            }
221
        }
222
223
        $declared = get_declared_classes();
224
225
        foreach ($declared as $className) {
226
            $rc         = new \ReflectionClass($className);
227
            $sourceFile = $rc->getFileName();
228
            if (in_array($sourceFile, $includedFiles, true) && ! $this->isTransient($className)) {
229
                $classes[] = $className;
230
            }
231
        }
232
233
        $this->classNames = $classes;
234
235
        return $classes;
236
    }
237
238
    /**
239
     * {@inheritdoc}
240
     *
241
     * @throws \UnexpectedValueException
242
     * @throws \ReflectionException
243
     * @throws Mapping\MappingException
244
     */
245
    public function loadMetadataForClass(
246
        string $className,
247
        Mapping\ClassMetadata $metadata,
248
        Mapping\ClassMetadataBuildingContext $metadataBuildingContext
249
    ) : Mapping\ClassMetadata {
250
        $reflectionClass = $metadata->getReflectionClass();
251
252
        if (! $reflectionClass) {
253
            // this happens when running annotation driver in combination with
254
            // static reflection services. This is not the nicest fix
255
            $reflectionClass = new \ReflectionClass($metadata->getClassName());
256
        }
257
258
        $classAnnotations = $this->getClassAnnotations($reflectionClass);
259
        $classMetadata    = $this->convertClassAnnotationsToClassMetadata(
260
            $reflectionClass,
261
            $classAnnotations,
262
            $metadata,
263
            $metadataBuildingContext
264
        );
265
266
        // Evaluate @Cache annotation
267
        if (isset($classAnnotations[Annotation\Cache::class])) {
268
            $cacheAnnot = $classAnnotations[Annotation\Cache::class];
269
            $cache      = $this->convertCacheAnnotationToCacheMetadata($cacheAnnot, $metadata);
270
271
            $classMetadata->setCache($cache);
272
        }
273
274
        // Evaluate annotations on properties/fields
275
        /* @var $reflProperty \ReflectionProperty */
276
        foreach ($reflectionClass->getProperties() as $reflectionProperty) {
277
            if ($reflectionProperty->getDeclaringClass()->getName() !== $reflectionClass->getName()) {
278
                continue;
279
            }
280
281
            $propertyAnnotations = $this->getPropertyAnnotations($reflectionProperty);
282
            $property            = $this->convertPropertyAnnotationsToProperty(
283
                $propertyAnnotations,
284
                $reflectionProperty,
285
                $classMetadata,
286
                $metadataBuildingContext
287
            );
288
289
            if (! $property) {
290
                continue;
291
            }
292
293
            $metadata->addProperty($property);
294
        }
295
296
        $this->attachPropertyOverrides($classAnnotations, $reflectionClass, $metadata, $metadataBuildingContext);
297
298
        return $classMetadata;
299
    }
300
301
    /**
302
     * @param Annotation\Annotation[] $classAnnotations
303
     *
304
     * @throws \UnexpectedValueException
305
     * @throws Mapping\MappingException
306
     */
307
    private function convertClassAnnotationsToClassMetadata(
308
        \ReflectionClass $reflectionClass,
309
        array $classAnnotations,
310
        Mapping\ClassMetadata $metadata,
311
        Mapping\ClassMetadataBuildingContext $metadataBuildingContext
312
    ) : Mapping\ClassMetadata {
313
        switch (true) {
314
            case isset($classAnnotations[Annotation\Entity::class]):
315
                $binder = new Binder\EntityClassMetadataBinder($classAnnotations, $metadata);
316
317
                break;
318
319
            case isset($classAnnotations[Annotation\MappedSuperclass::class]):
320
                $binder = new Binder\MappedSuperClassMetadataBinder(
321
                    $reflectionClass,
322
                    $classAnnotations,
323
                    $metadata,
324
                    $metadataBuildingContext
325
                );
326
                break;
327
328
            case isset($classAnnotations[Annotation\Embeddable::class]):
329
                $binder = new Binder\EmbeddableClassMetadataBinder(
330
                    $reflectionClass,
331
                    $classAnnotations,
332
                    $metadata,
333
                    $metadataBuildingContext
334
                );
335
                break;
336
337
            default:
338
                throw Mapping\MappingException::classIsNotAValidEntityOrMappedSuperClass($reflectionClass->getName());
339
        }
340
341
        return $binder->bind();
342
    }
343
344
    /**
345
     * @param Annotation\Annotation[] $propertyAnnotations
346
     *
347
     * @todo guilhermeblanco Remove nullable typehint once embeddables are back
348
     *
349
     * @throws Mapping\MappingException
350
     */
351
    private function convertPropertyAnnotationsToProperty(
352
        array $propertyAnnotations,
353
        \ReflectionProperty $reflectionProperty,
354
        Mapping\ClassMetadata $metadata,
355
        Mapping\ClassMetadataBuildingContext $metadataBuildingContext
356
    ) : ?Mapping\Property {
357
        switch (true) {
358
            case isset($propertyAnnotations[Annotation\Column::class]):
0 ignored issues
show
Coding Style introduced by
case statements should be defined using a colon.

As per the PSR-2 coding standard, case statements should not be wrapped in curly braces. There is no need for braces, since each case is terminated by the next break.

There is also the option to use a semicolon instead of a colon, this is discouraged because many programmers do not even know it works and the colon is universal between programming languages.

switch ($expr) {
    case "A": { //wrong
        doSomething();
        break;
    }
    case "B"; //wrong
        doSomething();
        break;
    case "C": //right
        doSomething();
        break;
}

To learn more about the PSR-2 coding standard, please refer to the PHP-Fig.

Loading history...
359
                return $this->convertReflectionPropertyToFieldMetadata(
360
                    $reflectionProperty,
361
                    $propertyAnnotations,
362
                    $metadata,
363
                    $metadataBuildingContext
364
                );
365
366
            case isset($propertyAnnotations[Annotation\OneToOne::class]):
367
                return $this->convertReflectionPropertyToOneToOneAssociationMetadata(
368
                    $reflectionProperty,
369
                    $propertyAnnotations,
370
                    $metadata,
371
                    $metadataBuildingContext
372
                );
373
374
            case isset($propertyAnnotations[Annotation\ManyToOne::class]):
375
                return $this->convertReflectionPropertyToManyToOneAssociationMetadata(
376
                    $reflectionProperty,
377
                    $propertyAnnotations,
378
                    $metadata,
379
                    $metadataBuildingContext
380
                );
381
382
            case isset($propertyAnnotations[Annotation\OneToMany::class]):
383
                return $this->convertReflectionPropertyToOneToManyAssociationMetadata(
384
                    $reflectionProperty,
385
                    $propertyAnnotations,
386
                    $metadata,
387
                    $metadataBuildingContext
388
                );
389
390
            case isset($propertyAnnotations[Annotation\ManyToMany::class]):
391
                return $this->convertReflectionPropertyToManyToManyAssociationMetadata(
392
                    $reflectionProperty,
393
                    $propertyAnnotations,
394
                    $metadata,
395
                    $metadataBuildingContext
396
                );
397
398
            case isset($propertyAnnotations[Annotation\Embedded::class]):
399
                return null;
400
401
            default:
402
                return new Mapping\TransientMetadata($reflectionProperty->getName());
403
        }
404
    }
405
406
    /**
407
     * @param Annotation\Annotation[] $propertyAnnotations
408
     *
409
     * @throws Mapping\MappingException
410
     */
411
    private function convertReflectionPropertyToFieldMetadata(
412
        \ReflectionProperty $reflProperty,
413
        array $propertyAnnotations,
414
        Mapping\ClassMetadata $metadata,
415
        Mapping\ClassMetadataBuildingContext $metadataBuildingContext
416
    ) : Mapping\FieldMetadata {
417
        $className   = $metadata->getClassName();
418
        $fieldName   = $reflProperty->getName();
419
        $isVersioned = isset($propertyAnnotations[Annotation\Version::class]);
420
        $columnAnnot = $propertyAnnotations[Annotation\Column::class];
421
422
        if ($columnAnnot->type === null) {
423
            throw Mapping\MappingException::propertyTypeIsRequired($className, $fieldName);
424
        }
425
426
        $fieldMetadata = $this->convertColumnAnnotationToFieldMetadata($columnAnnot, $fieldName, $isVersioned);
427
428
        // Check for Id
429
        if (isset($propertyAnnotations[Annotation\Id::class])) {
430
            $fieldMetadata->setPrimaryKey(true);
431
        }
432
433
        // Check for GeneratedValue strategy
434
        if (isset($propertyAnnotations[Annotation\GeneratedValue::class])) {
435
            $generatedValueAnnot = $propertyAnnotations[Annotation\GeneratedValue::class];
436
            $strategy            = strtoupper($generatedValueAnnot->strategy);
437
            $idGeneratorType     = constant(sprintf('%s::%s', Mapping\GeneratorType::class, $strategy));
438
439
            if ($idGeneratorType !== Mapping\GeneratorType::NONE) {
440
                $idGeneratorDefinition = [];
441
442
                // Check for CustomGenerator/SequenceGenerator/TableGenerator definition
443
                switch (true) {
444
                    case isset($propertyAnnotations[Annotation\SequenceGenerator::class]):
445
                        $seqGeneratorAnnot = $propertyAnnotations[Annotation\SequenceGenerator::class];
446
447
                        $idGeneratorDefinition = [
448
                            'sequenceName' => $seqGeneratorAnnot->sequenceName,
449
                            'allocationSize' => $seqGeneratorAnnot->allocationSize,
450
                        ];
451
452
                        break;
453
454
                    case isset($propertyAnnotations[Annotation\CustomIdGenerator::class]):
455
                        $customGeneratorAnnot = $propertyAnnotations[Annotation\CustomIdGenerator::class];
456
457
                        $idGeneratorDefinition = [
458
                            'class' => $customGeneratorAnnot->class,
459
                            'arguments' => $customGeneratorAnnot->arguments,
460
                        ];
461
462
                        break;
463
464
                    /* @todo If it is not supported, why does this exist? */
465
                    case isset($propertyAnnotations['Doctrine\ORM\Mapping\TableGenerator']):
466
                        throw Mapping\MappingException::tableIdGeneratorNotImplemented($className);
467
                }
468
469
                $fieldMetadata->setValueGenerator(new Mapping\ValueGeneratorMetadata($idGeneratorType, $idGeneratorDefinition));
470
            }
471
        }
472
473
        return $fieldMetadata;
474
    }
475
476
    /**
477
     * @param Annotation\Annotation[] $propertyAnnotations
478
     *
479
     * @return Mapping\OneToOneAssociationMetadata
480
     */
481
    private function convertReflectionPropertyToOneToOneAssociationMetadata(
482
        \ReflectionProperty $reflectionProperty,
483
        array $propertyAnnotations,
484
        Mapping\ClassMetadata $metadata,
485
        Mapping\ClassMetadataBuildingContext $metadataBuildingContext
486
    ) {
487
        $className     = $metadata->getClassName();
488
        $fieldName     = $reflectionProperty->getName();
489
        $oneToOneAnnot = $propertyAnnotations[Annotation\OneToOne::class];
490
        $assocMetadata = new Mapping\OneToOneAssociationMetadata($fieldName);
491
        $targetEntity  = $oneToOneAnnot->targetEntity;
492
493
        $assocMetadata->setTargetEntity($targetEntity);
494
        $assocMetadata->setCascade($this->getCascade($className, $fieldName, $oneToOneAnnot->cascade));
495
        $assocMetadata->setOrphanRemoval($oneToOneAnnot->orphanRemoval);
496
        $assocMetadata->setFetchMode($this->getFetchMode($className, $oneToOneAnnot->fetch));
497
498
        if (! empty($oneToOneAnnot->mappedBy)) {
499
            $assocMetadata->setMappedBy($oneToOneAnnot->mappedBy);
500
        }
501
502
        if (! empty($oneToOneAnnot->inversedBy)) {
503
            $assocMetadata->setInversedBy($oneToOneAnnot->inversedBy);
504
        }
505
506
        // Check for Id
507
        if (isset($propertyAnnotations[Annotation\Id::class])) {
508
            $assocMetadata->setPrimaryKey(true);
509
        }
510
511
        $this->attachAssociationPropertyCache($propertyAnnotations, $reflectionProperty, $assocMetadata, $metadata);
512
513
        // Check for JoinColumn/JoinColumns annotations
514
        switch (true) {
515
            case isset($propertyAnnotations[Annotation\JoinColumn::class]):
516
                $joinColumnAnnot = $propertyAnnotations[Annotation\JoinColumn::class];
517
518
                $assocMetadata->addJoinColumn(
519
                    $this->convertJoinColumnAnnotationToJoinColumnMetadata($joinColumnAnnot)
520
                );
521
522
                break;
523
524
            case isset($propertyAnnotations[Annotation\JoinColumns::class]):
525
                $joinColumnsAnnot = $propertyAnnotations[Annotation\JoinColumns::class];
526
527
                foreach ($joinColumnsAnnot->value as $joinColumnAnnot) {
528
                    $assocMetadata->addJoinColumn(
529
                        $this->convertJoinColumnAnnotationToJoinColumnMetadata($joinColumnAnnot)
530
                    );
531
                }
532
533
                break;
534
        }
535
536
        return $assocMetadata;
537
    }
538
539
    /**
540
     * @param Annotation\Annotation[] $propertyAnnotations
541
     *
542
     * @return Mapping\ManyToOneAssociationMetadata
543
     */
544
    private function convertReflectionPropertyToManyToOneAssociationMetadata(
545
        \ReflectionProperty $reflectionProperty,
546
        array $propertyAnnotations,
547
        Mapping\ClassMetadata $metadata,
548
        Mapping\ClassMetadataBuildingContext $metadataBuildingContext
549
    ) {
550
        $className      = $metadata->getClassName();
551
        $fieldName      = $reflectionProperty->getName();
552
        $manyToOneAnnot = $propertyAnnotations[Annotation\ManyToOne::class];
553
        $assocMetadata  = new Mapping\ManyToOneAssociationMetadata($fieldName);
554
        $targetEntity   = $manyToOneAnnot->targetEntity;
555
556
        $assocMetadata->setTargetEntity($targetEntity);
557
        $assocMetadata->setCascade($this->getCascade($className, $fieldName, $manyToOneAnnot->cascade));
558
        $assocMetadata->setFetchMode($this->getFetchMode($className, $manyToOneAnnot->fetch));
559
560
        if (! empty($manyToOneAnnot->inversedBy)) {
561
            $assocMetadata->setInversedBy($manyToOneAnnot->inversedBy);
562
        }
563
564
        // Check for Id
565
        if (isset($propertyAnnotations[Annotation\Id::class])) {
566
            $assocMetadata->setPrimaryKey(true);
567
        }
568
569
        $this->attachAssociationPropertyCache($propertyAnnotations, $reflectionProperty, $assocMetadata, $metadata);
570
571
        // Check for JoinColumn/JoinColumns annotations
572
        switch (true) {
573
            case isset($propertyAnnotations[Annotation\JoinColumn::class]):
574
                $joinColumnAnnot = $propertyAnnotations[Annotation\JoinColumn::class];
575
576
                $assocMetadata->addJoinColumn(
577
                    $this->convertJoinColumnAnnotationToJoinColumnMetadata($joinColumnAnnot)
578
                );
579
580
                break;
581
582
            case isset($propertyAnnotations[Annotation\JoinColumns::class]):
583
                $joinColumnsAnnot = $propertyAnnotations[Annotation\JoinColumns::class];
584
585
                foreach ($joinColumnsAnnot->value as $joinColumnAnnot) {
586
                    $assocMetadata->addJoinColumn(
587
                        $this->convertJoinColumnAnnotationToJoinColumnMetadata($joinColumnAnnot)
588
                    );
589
                }
590
591
                break;
592
        }
593
594
        return $assocMetadata;
595
    }
596
597
    /**
598
     * @param Annotation\Annotation[] $propertyAnnotations
599
     *
600
     * @throws Mapping\MappingException
601
     */
602
    private function convertReflectionPropertyToOneToManyAssociationMetadata(
603
        \ReflectionProperty $reflectionProperty,
604
        array $propertyAnnotations,
605
        Mapping\ClassMetadata $metadata,
606
        Mapping\ClassMetadataBuildingContext $metadataBuildingContext
607
    ) : Mapping\OneToManyAssociationMetadata {
608
        $className      = $metadata->getClassName();
609
        $fieldName      = $reflectionProperty->getName();
610
        $oneToManyAnnot = $propertyAnnotations[Annotation\OneToMany::class];
611
        $assocMetadata  = new Mapping\OneToManyAssociationMetadata($fieldName);
612
        $targetEntity   = $oneToManyAnnot->targetEntity;
613
614
        $assocMetadata->setTargetEntity($targetEntity);
615
        $assocMetadata->setCascade($this->getCascade($className, $fieldName, $oneToManyAnnot->cascade));
616
        $assocMetadata->setOrphanRemoval($oneToManyAnnot->orphanRemoval);
617
        $assocMetadata->setFetchMode($this->getFetchMode($className, $oneToManyAnnot->fetch));
618
619
        if (! empty($oneToManyAnnot->mappedBy)) {
620
            $assocMetadata->setMappedBy($oneToManyAnnot->mappedBy);
621
        }
622
623
        if (! empty($oneToManyAnnot->indexBy)) {
624
            $assocMetadata->setIndexedBy($oneToManyAnnot->indexBy);
625
        }
626
627
        // Check for OrderBy
628
        if (isset($propertyAnnotations[Annotation\OrderBy::class])) {
629
            $orderByAnnot = $propertyAnnotations[Annotation\OrderBy::class];
630
631
            $assocMetadata->setOrderBy($orderByAnnot->value);
632
        }
633
634
        // Check for Id
635
        if (isset($propertyAnnotations[Annotation\Id::class])) {
636
            throw Mapping\MappingException::illegalToManyIdentifierAssociation($className, $fieldName);
637
        }
638
639
        $this->attachAssociationPropertyCache($propertyAnnotations, $reflectionProperty, $assocMetadata, $metadata);
640
641
        return $assocMetadata;
642
    }
643
644
    /**
645
     * @param Annotation\Annotation[] $propertyAnnotations
646
     *
647
     * @throws Mapping\MappingException
648
     */
649
    private function convertReflectionPropertyToManyToManyAssociationMetadata(
650
        \ReflectionProperty $reflectionProperty,
651
        array $propertyAnnotations,
652
        Mapping\ClassMetadata $metadata,
653
        Mapping\ClassMetadataBuildingContext $metadataBuildingContext
654
    ) : Mapping\ManyToManyAssociationMetadata {
655
        $className       = $metadata->getClassName();
656
        $fieldName       = $reflectionProperty->getName();
657
        $manyToManyAnnot = $propertyAnnotations[Annotation\ManyToMany::class];
658
        $assocMetadata   = new Mapping\ManyToManyAssociationMetadata($fieldName);
659
        $targetEntity    = $manyToManyAnnot->targetEntity;
660
661
        $assocMetadata->setTargetEntity($targetEntity);
662
        $assocMetadata->setCascade($this->getCascade($className, $fieldName, $manyToManyAnnot->cascade));
663
        $assocMetadata->setOrphanRemoval($manyToManyAnnot->orphanRemoval);
664
        $assocMetadata->setFetchMode($this->getFetchMode($className, $manyToManyAnnot->fetch));
665
666
        if (! empty($manyToManyAnnot->mappedBy)) {
667
            $assocMetadata->setMappedBy($manyToManyAnnot->mappedBy);
668
        }
669
670
        if (! empty($manyToManyAnnot->inversedBy)) {
671
            $assocMetadata->setInversedBy($manyToManyAnnot->inversedBy);
672
        }
673
674
        if (! empty($manyToManyAnnot->indexBy)) {
675
            $assocMetadata->setIndexedBy($manyToManyAnnot->indexBy);
676
        }
677
678
        // Check for JoinTable
679
        if (isset($propertyAnnotations[Annotation\JoinTable::class])) {
680
            $joinTableAnnot    = $propertyAnnotations[Annotation\JoinTable::class];
681
            $joinTableMetadata = $this->convertJoinTableAnnotationToJoinTableMetadata($joinTableAnnot);
682
683
            $assocMetadata->setJoinTable($joinTableMetadata);
684
        }
685
686
        // Check for OrderBy
687
        if (isset($propertyAnnotations[Annotation\OrderBy::class])) {
688
            $orderByAnnot = $propertyAnnotations[Annotation\OrderBy::class];
689
690
            $assocMetadata->setOrderBy($orderByAnnot->value);
691
        }
692
693
        // Check for Id
694
        if (isset($propertyAnnotations[Annotation\Id::class])) {
695
            throw Mapping\MappingException::illegalToManyIdentifierAssociation($className, $fieldName);
696
        }
697
698
        $this->attachAssociationPropertyCache($propertyAnnotations, $reflectionProperty, $assocMetadata, $metadata);
699
700
        return $assocMetadata;
701
    }
702
703
    /**
704
     * Parse the given Column as FieldMetadata
705
     */
706
    private function convertColumnAnnotationToFieldMetadata(
707
        Annotation\Column $columnAnnot,
708
        string $fieldName,
709
        bool $isVersioned
710
    ) : Mapping\FieldMetadata {
711
        $fieldMetadata = $isVersioned
712
            ? new Mapping\VersionFieldMetadata($fieldName)
713
            : new Mapping\FieldMetadata($fieldName)
714
        ;
715
716
        $fieldMetadata->setType(Type::getType($columnAnnot->type));
717
718
        if (! empty($columnAnnot->name)) {
719
            $fieldMetadata->setColumnName($columnAnnot->name);
720
        }
721
722
        if (! empty($columnAnnot->columnDefinition)) {
723
            $fieldMetadata->setColumnDefinition($columnAnnot->columnDefinition);
724
        }
725
726
        if (! empty($columnAnnot->length)) {
727
            $fieldMetadata->setLength($columnAnnot->length);
728
        }
729
730
        if ($columnAnnot->options) {
731
            $fieldMetadata->setOptions($columnAnnot->options);
732
        }
733
734
        $fieldMetadata->setScale($columnAnnot->scale);
735
        $fieldMetadata->setPrecision($columnAnnot->precision);
736
        $fieldMetadata->setNullable($columnAnnot->nullable);
737
        $fieldMetadata->setUnique($columnAnnot->unique);
738
739
        return $fieldMetadata;
740
    }
741
742
    /**
743
     * Parse the given Table as TableMetadata
744
     */
745
    private function convertTableAnnotationToTableMetadata(
746
        Annotation\Table $tableAnnot,
747
        Mapping\TableMetadata $table
748
    ) : Mapping\TableMetadata {
749
        if (! empty($tableAnnot->name)) {
750
            $table->setName($tableAnnot->name);
751
        }
752
753
        if (! empty($tableAnnot->schema)) {
754
            $table->setSchema($tableAnnot->schema);
755
        }
756
757
        foreach ($tableAnnot->options as $optionName => $optionValue) {
758
            $table->addOption($optionName, $optionValue);
759
        }
760
761
        foreach ($tableAnnot->indexes as $indexAnnot) {
762
            $table->addIndex([
763
                'name'    => $indexAnnot->name,
764
                'columns' => $indexAnnot->columns,
765
                'unique'  => $indexAnnot->unique,
766
                'options' => $indexAnnot->options,
767
                'flags'   => $indexAnnot->flags,
768
            ]);
769
        }
770
771
        foreach ($tableAnnot->uniqueConstraints as $uniqueConstraintAnnot) {
772
            $table->addUniqueConstraint([
773
                'name'    => $uniqueConstraintAnnot->name,
774
                'columns' => $uniqueConstraintAnnot->columns,
775
                'options' => $uniqueConstraintAnnot->options,
776
                'flags'   => $uniqueConstraintAnnot->flags,
777
            ]);
778
        }
779
780
        return $table;
781
    }
782
783
    /**
784
     * Parse the given JoinTable as JoinTableMetadata
785
     */
786
    private function convertJoinTableAnnotationToJoinTableMetadata(
787
        Annotation\JoinTable $joinTableAnnot
788
    ) : Mapping\JoinTableMetadata {
789
        $joinTable = new Mapping\JoinTableMetadata();
790
791
        if (! empty($joinTableAnnot->name)) {
792
            $joinTable->setName($joinTableAnnot->name);
793
        }
794
795
        if (! empty($joinTableAnnot->schema)) {
796
            $joinTable->setSchema($joinTableAnnot->schema);
797
        }
798
799
        foreach ($joinTableAnnot->joinColumns as $joinColumnAnnot) {
800
            $joinColumn = $this->convertJoinColumnAnnotationToJoinColumnMetadata($joinColumnAnnot);
801
802
            $joinTable->addJoinColumn($joinColumn);
803
        }
804
805
        foreach ($joinTableAnnot->inverseJoinColumns as $joinColumnAnnot) {
806
            $joinColumn = $this->convertJoinColumnAnnotationToJoinColumnMetadata($joinColumnAnnot);
807
808
            $joinTable->addInverseJoinColumn($joinColumn);
809
        }
810
811
        return $joinTable;
812
    }
813
814
    /**
815
     * Parse the given JoinColumn as JoinColumnMetadata
816
     */
817
    private function convertJoinColumnAnnotationToJoinColumnMetadata(
818
        Annotation\JoinColumn $joinColumnAnnot
819
    ) : Mapping\JoinColumnMetadata {
820
        $joinColumn = new Mapping\JoinColumnMetadata();
821
822
        // @todo Remove conditionals for name and referencedColumnName once naming strategy is brought into drivers
823
        if (! empty($joinColumnAnnot->name)) {
824
            $joinColumn->setColumnName($joinColumnAnnot->name);
825
        }
826
827
        if (! empty($joinColumnAnnot->referencedColumnName)) {
828
            $joinColumn->setReferencedColumnName($joinColumnAnnot->referencedColumnName);
829
        }
830
831
        $joinColumn->setNullable($joinColumnAnnot->nullable);
832
        $joinColumn->setUnique($joinColumnAnnot->unique);
833
834
        if (! empty($joinColumnAnnot->fieldName)) {
835
            $joinColumn->setAliasedName($joinColumnAnnot->fieldName);
836
        }
837
838
        if (! empty($joinColumnAnnot->columnDefinition)) {
839
            $joinColumn->setColumnDefinition($joinColumnAnnot->columnDefinition);
840
        }
841
842
        if ($joinColumnAnnot->onDelete) {
843
            $joinColumn->setOnDelete(strtoupper($joinColumnAnnot->onDelete));
844
        }
845
846
        return $joinColumn;
847
    }
848
849
    /**
850
     * Parse the given Cache as CacheMetadata
851
     *
852
     * @param string|null $fieldName
853
     */
854
    private function convertCacheAnnotationToCacheMetadata(
855
        Annotation\Cache $cacheAnnot,
856
        Mapping\ClassMetadata $metadata,
857
        $fieldName = null
858
    ) : Mapping\CacheMetadata {
859
        $baseRegion    = strtolower(str_replace('\\', '_', $metadata->getRootClassName()));
860
        $defaultRegion = $baseRegion . ($fieldName ? '__' . $fieldName : '');
861
862
        $usage  = constant(sprintf('%s::%s', Mapping\CacheUsage::class, $cacheAnnot->usage));
863
        $region = $cacheAnnot->region ?: $defaultRegion;
864
865
        return new Mapping\CacheMetadata($usage, $region);
866
    }
867
868
    /**
869
     * @param Annotation\Annotation[] $classAnnotations
870
     */
871
    private function attachTable(
872
        array $classAnnotations,
873
        \ReflectionClass $reflectionClass,
874
        Mapping\ClassMetadata $metadata,
875
        Mapping\ClassMetadataBuildingContext $metadataBuildingContext
876
    ) : void {
877
        $parent = $metadata->getParent();
878
879
        if ($parent !== null && $parent->inheritanceType === Mapping\InheritanceType::SINGLE_TABLE) {
880
            $metadata->setTable($parent->table);
881
882
            return;
883
        }
884
885
        $namingStrategy = $metadataBuildingContext->getNamingStrategy();
886
        $table          = new Mapping\TableMetadata();
887
888
        $table->setName($namingStrategy->classToTableName($metadata->getClassName()));
889
890
        // Evaluate @Table annotation
891
        if (isset($classAnnotations[Annotation\Table::class])) {
892
            $tableAnnot = $classAnnotations[Annotation\Table::class];
893
894
            $this->convertTableAnnotationToTableMetadata($table, $tableAnnot);
895
        }
896
897
        $metadata->setTable($table);
898
    }
899
900
    /**
901
     * @param Annotation\Annotation[] $classAnnotations
902
     *
903
     * @throws Mapping\MappingException
904
     */
905
    private function attachPropertyOverrides(
906
        array $classAnnotations,
907
        \ReflectionClass $reflectionClass,
908
        Mapping\ClassMetadata $metadata,
909
        Mapping\ClassMetadataBuildingContext $metadataBuildingContext
910
    ) : void {
911
        // Evaluate AssociationOverrides annotation
912
        if (isset($classAnnotations[Annotation\AssociationOverrides::class])) {
913
            $associationOverridesAnnot = $classAnnotations[Annotation\AssociationOverrides::class];
914
915
            foreach ($associationOverridesAnnot->value as $associationOverride) {
916
                $fieldName = $associationOverride->name;
917
                $property  = $metadata->getProperty($fieldName);
918
919
                if (! $property) {
920
                    throw Mapping\MappingException::invalidOverrideFieldName($metadata->getClassName(), $fieldName);
921
                }
922
923
                $existingClass = get_class($property);
924
                $override      = new $existingClass($fieldName);
925
926
                // Check for JoinColumn/JoinColumns annotations
927
                if ($associationOverride->joinColumns) {
928
                    $joinColumns = [];
929
930
                    foreach ($associationOverride->joinColumns as $joinColumnAnnot) {
931
                        $joinColumns[] = $this->convertJoinColumnAnnotationToJoinColumnMetadata($joinColumnAnnot);
932
                    }
933
934
                    $override->setJoinColumns($joinColumns);
935
                }
936
937
                // Check for JoinTable annotations
938
                if ($associationOverride->joinTable) {
939
                    $joinTableAnnot    = $associationOverride->joinTable;
940
                    $joinTableMetadata = $this->convertJoinTableAnnotationToJoinTableMetadata($joinTableAnnot);
941
942
                    $override->setJoinTable($joinTableMetadata);
943
                }
944
945
                // Check for inversedBy
946
                if ($associationOverride->inversedBy) {
947
                    $override->setInversedBy($associationOverride->inversedBy);
948
                }
949
950
                // Check for fetch
951
                if ($associationOverride->fetch) {
952
                    $override->setFetchMode(
953
                        constant(Mapping\FetchMode::class . '::' . $associationOverride->fetch)
954
                    );
955
                }
956
957
                $metadata->setPropertyOverride($override);
958
            }
959
        }
960
961
        // Evaluate AttributeOverrides annotation
962
        if (isset($classAnnotations[Annotation\AttributeOverrides::class])) {
963
            $attributeOverridesAnnot = $classAnnotations[Annotation\AttributeOverrides::class];
964
965
            foreach ($attributeOverridesAnnot->value as $attributeOverrideAnnot) {
966
                $fieldMetadata = $this->convertColumnAnnotationToFieldMetadata(
967
                    $attributeOverrideAnnot->column,
968
                    $attributeOverrideAnnot->name,
969
                    false
970
                );
971
972
                $metadata->setPropertyOverride($fieldMetadata);
973
            }
974
        }
975
    }
976
977
    /**
978
     * @param Annotation\Annotation[] $propertyAnnotations
979
     */
980
    private function attachAssociationPropertyCache(
981
        array $propertyAnnotations,
982
        \ReflectionProperty $reflectionProperty,
983
        Mapping\AssociationMetadata $assocMetadata,
984
        Mapping\ClassMetadata $metadata
985
    ) : void {
986
        // Check for Cache
987
        if (isset($propertyAnnotations[Annotation\Cache::class])) {
988
            $cacheAnnot    = $propertyAnnotations[Annotation\Cache::class];
989
            $cacheMetadata = $this->convertCacheAnnotationToCacheMetadata(
990
                $cacheAnnot,
991
                $metadata,
992
                $reflectionProperty->getName()
993
            );
994
995
            $assocMetadata->setCache($cacheMetadata);
996
        }
997
    }
998
999
    /**
1000
     * Attempts to resolve the cascade modes.
1001
     *
1002
     * @param string   $className        The class name.
1003
     * @param string   $fieldName        The field name.
1004
     * @param string[] $originalCascades The original unprocessed field cascades.
1005
     *
1006
     * @return string[] The processed field cascades.
1007
     *
1008
     * @throws Mapping\MappingException If a cascade option is not valid.
1009
     */
1010
    private function getCascade(string $className, string $fieldName, array $originalCascades) : array
1011
    {
1012
        $cascadeTypes = ['remove', 'persist', 'refresh'];
1013
        $cascades     = array_map('strtolower', $originalCascades);
1014
1015
        if (in_array('all', $cascades, true)) {
1016
            $cascades = $cascadeTypes;
1017
        }
1018
1019
        if (count($cascades) !== count(array_intersect($cascades, $cascadeTypes))) {
1020
            $diffCascades = array_diff($cascades, array_intersect($cascades, $cascadeTypes));
1021
1022
            throw Mapping\MappingException::invalidCascadeOption($diffCascades, $className, $fieldName);
1023
        }
1024
1025
        return $cascades;
1026
    }
1027
1028
    /**
1029
     * Attempts to resolve the fetch mode.
1030
     *
1031
     * @param string $className The class name.
1032
     * @param string $fetchMode The fetch mode.
1033
     *
1034
     * @return string The fetch mode as defined in ClassMetadata.
1035
     *
1036
     * @throws Mapping\MappingException If the fetch mode is not valid.
1037
     */
1038
    private function getFetchMode($className, $fetchMode) : string
1039
    {
1040
        $fetchModeConstant = sprintf('%s::%s', Mapping\FetchMode::class, $fetchMode);
1041
1042
        if (! defined($fetchModeConstant)) {
1043
            throw Mapping\MappingException::invalidFetchMode($className, $fetchMode);
1044
        }
1045
1046
        return constant($fetchModeConstant);
1047
    }
1048
1049
    /**
1050
     * Parses the given method.
1051
     *
1052
     * @return string[]
1053
     */
1054
    private function getMethodCallbacks(\ReflectionMethod $method) : array
1055
    {
1056
        $annotations = $this->getMethodAnnotations($method);
1057
        $events      = [
1058
            Events::prePersist  => Annotation\PrePersist::class,
1059
            Events::postPersist => Annotation\PostPersist::class,
1060
            Events::preUpdate   => Annotation\PreUpdate::class,
1061
            Events::postUpdate  => Annotation\PostUpdate::class,
1062
            Events::preRemove   => Annotation\PreRemove::class,
1063
            Events::postRemove  => Annotation\PostRemove::class,
1064
            Events::postLoad    => Annotation\PostLoad::class,
1065
            Events::preFlush    => Annotation\PreFlush::class,
1066
        ];
1067
1068
        // Check for callbacks
1069
        $callbacks = [];
1070
1071
        foreach ($events as $eventName => $annotationClassName) {
1072
            if (isset($annotations[$annotationClassName]) || $method->getName() === $eventName) {
1073
                $callbacks[] = $eventName;
1074
            }
1075
        }
1076
1077
        return $callbacks;
1078
    }
1079
1080
    /**
1081
     * @return Annotation\Annotation[]
1082
     */
1083
    private function getClassAnnotations(\ReflectionClass $reflectionClass) : array
1084
    {
1085
        $classAnnotations = $this->reader->getClassAnnotations($reflectionClass);
1086
1087
        foreach ($classAnnotations as $key => $annot) {
1088
            if (! is_numeric($key)) {
1089
                continue;
1090
            }
1091
1092
            $classAnnotations[get_class($annot)] = $annot;
1093
        }
1094
1095
        return $classAnnotations;
1096
    }
1097
1098
    /**
1099
     * @return Annotation\Annotation[]
1100
     */
1101
    private function getPropertyAnnotations(\ReflectionProperty $reflectionProperty) : array
1102
    {
1103
        $propertyAnnotations = $this->reader->getPropertyAnnotations($reflectionProperty);
1104
1105
        foreach ($propertyAnnotations as $key => $annot) {
1106
            if (! is_numeric($key)) {
1107
                continue;
1108
            }
1109
1110
            $propertyAnnotations[get_class($annot)] = $annot;
1111
        }
1112
1113
        return $propertyAnnotations;
1114
    }
1115
1116
    /**
1117
     * @return Annotation\Annotation[]
1118
     */
1119
    private function getMethodAnnotations(\ReflectionMethod $reflectionMethod) : array
1120
    {
1121
        $methodAnnotations = $this->reader->getMethodAnnotations($reflectionMethod);
1122
1123
        foreach ($methodAnnotations as $key => $annot) {
1124
            if (! is_numeric($key)) {
1125
                continue;
1126
            }
1127
1128
            $methodAnnotations[get_class($annot)] = $annot;
1129
        }
1130
1131
        return $methodAnnotations;
1132
    }
1133
1134
    /**
1135
     * Factory method for the Annotation Driver.
1136
     *
1137
     * @param string|string[] $paths
1138
     *
1139
     * @return AnnotationDriver
1140
     */
1141
    public static function create(array $paths = [], ?AnnotationReader $reader = null)
1142
    {
1143
        if ($reader === null) {
1144
            $reader = new AnnotationReader();
1145
        }
1146
1147
        return new self($reader, $paths);
1148
    }
1149
}
1150