Failed Conditions
Push — master ( e747f7...5b15a6 )
by Guilherme
19:57
created

ORM/Mapping/Driver/NewAnnotationDriver.php (2 issues)

1
<?php
2
3
declare(strict_types=1);
4
5
namespace Doctrine\ORM\Mapping\Driver;
6
7
use Doctrine\Common\Annotations\AnnotationReader;
8
use Doctrine\Common\Persistence\Mapping\Driver\FileLocator;
9
use Doctrine\DBAL\Types\Type;
10
use Doctrine\ORM\Annotation;
11
use Doctrine\ORM\Events;
12
use Doctrine\ORM\Mapping;
13
use Doctrine\ORM\Mapping\Factory;
14
use ReflectionClass;
15
use ReflectionMethod;
16
use ReflectionProperty;
17
use UnexpectedValueException;
18
use function array_diff;
19
use function array_filter;
20
use function array_intersect;
21
use function array_map;
22
use function array_merge;
23
use function class_exists;
24
use function constant;
25
use function count;
26
use function defined;
27
use function get_class;
28
use function in_array;
29
use function is_numeric;
30
use function sprintf;
31
use function str_replace;
32
use function strtolower;
33
use function strtoupper;
34
35
class NewAnnotationDriver implements MappingDriver
36
{
37
    /** @var int[] */
38
    protected static $entityAnnotationClasses = [
39
        Annotation\Entity::class           => 1,
40
        Annotation\MappedSuperclass::class => 2,
41
    ];
42
43
    /**
44
     * The Annotation reader.
45
     *
46
     * @var AnnotationReader
47
     */
48
    protected $reader;
49
50
    /**
51
     * The file locator.
52
     *
53
     * @var FileLocator
54
     */
55
    protected $locator;
56
57
    /** @var Factory\NamingStrategy */
58
    protected $namingStrategy;
59
60
    /**
61
     * Cache for AnnotationDriver#getAllClassNames().
62
     *
63
     * @var string[]|null
64
     */
65
    private $classNames;
66
67
    /**
68
     * Initializes a new AnnotationDriver that uses the given AnnotationReader for reading docblock annotations.
69
     *
70
     * @param AnnotationReader       $reader         The AnnotationReader to use, duck-typed.
71
     * @param FileLocator            $locator        A FileLocator or one/multiple paths where mapping documents can be found.
72
     * @param Factory\NamingStrategy $namingStrategy The NamingStrategy to use.
73
     */
74
    public function __construct(AnnotationReader $reader, FileLocator $locator, Factory\NamingStrategy $namingStrategy)
75
    {
76
        $this->reader         = $reader;
77
        $this->locator        = $locator;
78
        $this->namingStrategy = $namingStrategy;
79
    }
80
81
    /**
82
     * {@inheritdoc}
83
     *
84
     * @return Mapping\ClassMetadata
85
     *
86
     * @throws Mapping\MappingException
87
     */
88
    public function loadMetadataForClass(
89
        string $className,
90
        Mapping\ClassMetadata $metadata,
91
        Mapping\ClassMetadataBuildingContext $metadataBuildingContext
92
    ) {
93
        // IMPORTANT: We're handling $metadata as "parent" metadata here, while building the $className ClassMetadata.
94
        $reflectionClass = new ReflectionClass($className);
95
96
        // Evaluate annotations on class metadata
97
        $classAnnotations = $this->getClassAnnotations($reflectionClass);
98
        $classMetadata    = $this->convertClassAnnotationsToClassMetadata(
99
            $classAnnotations,
100
            $reflectionClass,
101
            $metadata
102
        );
103
104
        // Evaluate @Cache annotation
105
        if (isset($classAnnotations[Annotation\Cache::class])) {
106
            $cacheAnnot = $classAnnotations[Annotation\Cache::class];
107
            $cache      = $this->convertCacheAnnotationToCacheMetadata($cacheAnnot, $classMetadata);
108
109
            $classMetadata->setCache($cache);
110
        }
111
112
        // Evaluate annotations on properties/fields
113
        /** @var ReflectionProperty $reflectionProperty */
114
        foreach ($reflectionClass->getProperties() as $reflectionProperty) {
115
            if ($reflectionProperty->getDeclaringClass()->getClassName() !== $reflectionClass->getName()) {
116
                continue;
117
            }
118
119
            $propertyAnnotations = $this->getPropertyAnnotations($reflectionProperty);
120
            $property            = $this->convertReflectionPropertyAnnotationsToProperty(
121
                $reflectionProperty,
122
                $propertyAnnotations,
123
                $classMetadata
124
            );
125
126
            $classMetadata->addDeclaredProperty($property);
127
        }
128
129
        return $classMetadata;
0 ignored issues
show
Bug Best Practice introduced by
The expression return $classMetadata returns the type Doctrine\ORM\Mapping\ClassMetadata which is incompatible with the return type mandated by Doctrine\ORM\Mapping\Dri...:loadMetadataForClass() of void.

In the issue above, the returned value is violating the contract defined by the mentioned interface.

Let's take a look at an example:

interface HasName {
    /** @return string */
    public function getName();
}

class Name {
    public $name;
}

class User implements HasName {
    /** @return string|Name */
    public function getName() {
        return new Name('foo'); // This is a violation of the ``HasName`` interface
                                // which only allows a string value to be returned.
    }
}
Loading history...
130
    }
131
132
    /**
133
     * {@inheritdoc}
134
     */
135
    public function getAllClassNames()
136
    {
137
        if ($this->classNames !== null) {
138
            return $this->classNames;
139
        }
140
141
        $classNames = array_filter(
142
            $this->locator->getAllClassNames(null),
143
            function ($className) {
144
                return ! $this->isTransient($className);
145
            }
146
        );
147
148
        $this->classNames = $classNames;
149
150
        return $classNames;
151
    }
152
153
    /**
154
     * {@inheritdoc}
155
     */
156
    public function isTransient($className)
157
    {
158
        $reflectionClass  = new ReflectionClass($className);
159
        $classAnnotations = $this->reader->getClassAnnotations($reflectionClass);
160
161
        foreach ($classAnnotations as $annotation) {
162
            if (isset(self::$entityAnnotationClasses[get_class($annotation)])) {
163
                return false;
164
            }
165
        }
166
167
        return true;
168
    }
169
170
    /**
171
     * @param Annotation\Annotation[] $classAnnotations
172
     *
173
     * @return Mapping\ClassMetadata|Mapping\ComponentMetadata
174
     *
175
     * @throws Mapping\MappingException
176
     */
177
    private function convertClassAnnotationsToClassMetadata(
178
        array $classAnnotations,
179
        ReflectionClass $reflectionClass,
180
        Mapping\ClassMetadata $parent
181
    ) {
182
        switch (true) {
183
            case isset($classAnnotations[Annotation\Entity::class]):
184
                return $this->convertClassAnnotationsToEntityClassMetadata(
185
                    $classAnnotations,
186
                    $reflectionClass,
187
                    $parent
188
                );
189
190
                break;
191
192
            case isset($classAnnotations[Annotation\MappedSuperclass::class]):
193
                return $this->convertClassAnnotationsToMappedSuperClassMetadata(
194
                    $classAnnotations,
195
                    $reflectionClass,
196
                    $parent
197
                );
198
            case isset($classAnnotations[Annotation\Embeddable::class]):
199
                return $this->convertClassAnnotationsToEntityClassMetadata(
200
                    $classAnnotations,
201
                    $reflectionClass,
202
                    $parent
203
                );
204
            default:
205
                throw Mapping\MappingException::classIsNotAValidEntityOrMappedSuperClass($reflectionClass->getName());
206
        }
207
    }
208
209
    /**
210
     * @param Annotation\Annotation[] $classAnnotations
211
     *
212
     * @return Mapping\ClassMetadata
213
     *
214
     * @throws Mapping\MappingException
215
     * @throws UnexpectedValueException
216
     */
217
    private function convertClassAnnotationsToEntityClassMetadata(
218
        array $classAnnotations,
219
        ReflectionClass $reflectionClass,
220
        Mapping\ClassMetadata $parent
221
    ) {
222
        /** @var Annotation\Entity $entityAnnot */
223
        $entityAnnot   = $classAnnotations[Annotation\Entity::class];
224
        $classMetadata = new Mapping\ClassMetadata($reflectionClass->getName(), $parent);
0 ignored issues
show
$parent of type Doctrine\ORM\Mapping\ClassMetadata is incompatible with the type Doctrine\ORM\Mapping\ClassMetadataBuildingContext expected by parameter $metadataBuildingContext of Doctrine\ORM\Mapping\ClassMetadata::__construct(). ( Ignorable by Annotation )

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

224
        $classMetadata = new Mapping\ClassMetadata($reflectionClass->getName(), /** @scrutinizer ignore-type */ $parent);
Loading history...
225
226
        if ($entityAnnot->repositoryClass !== null) {
227
            $classMetadata->setCustomRepositoryClassName($entityAnnot->repositoryClass);
228
        }
229
230
        if ($entityAnnot->readOnly) {
231
            $classMetadata->asReadOnly();
232
        }
233
234
        // Evaluate @Table annotation
235
        if (isset($classAnnotations[Annotation\Table::class])) {
236
            /** @var Annotation\Table $tableAnnot */
237
            $tableAnnot = $classAnnotations[Annotation\Table::class];
238
            $table      = $this->convertTableAnnotationToTableMetadata($tableAnnot);
239
240
            $classMetadata->setTable($table);
241
        }
242
243
        // Evaluate @ChangeTrackingPolicy annotation
244
        if (isset($classAnnotations[Annotation\ChangeTrackingPolicy::class])) {
245
            /** @var Annotation\ChangeTrackingPolicy $changeTrackingAnnot */
246
            $changeTrackingAnnot = $classAnnotations[Annotation\ChangeTrackingPolicy::class];
247
248
            $classMetadata->setChangeTrackingPolicy(
249
                constant(sprintf('%s::%s', Mapping\ChangeTrackingPolicy::class, $changeTrackingAnnot->value))
250
            );
251
        }
252
253
        // Evaluate @EntityListeners annotation
254
        if (isset($classAnnotations[Annotation\EntityListeners::class])) {
255
            /** @var Annotation\EntityListeners $entityListenersAnnot */
256
            $entityListenersAnnot = $classAnnotations[Annotation\EntityListeners::class];
257
258
            foreach ($entityListenersAnnot->value as $listenerClassName) {
259
                if (! class_exists($listenerClassName)) {
260
                    throw Mapping\MappingException::entityListenerClassNotFound(
261
                        $listenerClassName,
262
                        $reflectionClass->getName()
263
                    );
264
                }
265
266
                $listenerClass = new ReflectionClass($listenerClassName);
267
268
                /** @var ReflectionMethod $method */
269
                foreach ($listenerClass->getMethods(ReflectionMethod::IS_PUBLIC) as $method) {
270
                    foreach ($this->getMethodCallbacks($method) as $callback) {
271
                        $classMetadata->addEntityListener($callback, $listenerClassName, $method->getName());
272
                    }
273
                }
274
            }
275
        }
276
277
        // Evaluate @HasLifecycleCallbacks annotation
278
        if (isset($classAnnotations[Annotation\HasLifecycleCallbacks::class])) {
279
            /** @var ReflectionMethod $method */
280
            foreach ($reflectionClass->getMethods(ReflectionMethod::IS_PUBLIC) as $method) {
281
                foreach ($this->getMethodCallbacks($method) as $callback) {
282
                    $classMetadata->addLifecycleCallback($method->getName(), $callback);
283
                }
284
            }
285
        }
286
287
        // Evaluate @InheritanceType annotation
288
        if (isset($classAnnotations[Annotation\InheritanceType::class])) {
289
            /** @var Annotation\InheritanceType $inheritanceTypeAnnot */
290
            $inheritanceTypeAnnot = $classAnnotations[Annotation\InheritanceType::class];
291
292
            $classMetadata->setInheritanceType(
293
                constant(sprintf('%s::%s', Mapping\InheritanceType::class, $inheritanceTypeAnnot->value))
294
            );
295
296
            if ($classMetadata->inheritanceType !== Mapping\InheritanceType::NONE) {
297
                $discriminatorColumn = new Mapping\DiscriminatorColumnMetadata();
298
299
                // Evaluate @DiscriminatorColumn annotation
300
                if (isset($classAnnotations[Annotation\DiscriminatorColumn::class])) {
301
                    /** @var Annotation\DiscriminatorColumn $discriminatorColumnAnnot */
302
                    $discriminatorColumnAnnot = $classAnnotations[Annotation\DiscriminatorColumn::class];
303
304
                    $discriminatorColumn->setColumnName($discriminatorColumnAnnot->name);
305
306
                    if (! empty($discriminatorColumnAnnot->columnDefinition)) {
307
                        $discriminatorColumn->setColumnDefinition($discriminatorColumnAnnot->columnDefinition);
308
                    }
309
310
                    if (! empty($discriminatorColumnAnnot->type)) {
311
                        $discriminatorColumn->setType(Type::getType($discriminatorColumnAnnot->type));
312
                    }
313
314
                    if (! empty($discriminatorColumnAnnot->length)) {
315
                        $discriminatorColumn->setLength($discriminatorColumnAnnot->length);
316
                    }
317
                }
318
319
                if (empty($discriminatorColumn->getColumnName())) {
320
                    throw Mapping\MappingException::nameIsMandatoryForDiscriminatorColumns($reflectionClass->getName());
321
                }
322
323
                $classMetadata->setDiscriminatorColumn($discriminatorColumn);
324
325
                // Evaluate @DiscriminatorMap annotation
326
                if (isset($classAnnotations[Annotation\DiscriminatorMap::class])) {
327
                    /** @var Annotation\DiscriminatorMap $discriminatorMapAnnotation */
328
                    $discriminatorMapAnnotation = $classAnnotations[Annotation\DiscriminatorMap::class];
329
                    $discriminatorMap           = $discriminatorMapAnnotation->value;
330
331
                    $classMetadata->setDiscriminatorMap($discriminatorMap);
332
                }
333
            }
334
        }
335
336
        return $classMetadata;
337
    }
338
339
    /**
340
     * @param Annotation\Annotation[] $classAnnotations
341
     *
342
     * @return Mapping\MappedSuperClassMetadata
343
     */
344
    private function convertClassAnnotationsToMappedSuperClassMetadata(
345
        array $classAnnotations,
346
        ReflectionClass $reflectionClass,
347
        Mapping\ClassMetadata $parent
348
    ) {
349
        /** @var Annotation\MappedSuperclass $mappedSuperclassAnnot */
350
        $mappedSuperclassAnnot = $classAnnotations[Annotation\MappedSuperclass::class];
351
        $classMetadata         = new Mapping\MappedSuperClassMetadata($reflectionClass->getName(), $parent);
352
353
        if ($mappedSuperclassAnnot->repositoryClass !== null) {
354
            $classMetadata->setCustomRepositoryClassName($mappedSuperclassAnnot->repositoryClass);
355
        }
356
357
        return $classMetadata;
358
    }
359
360
    /**
361
     * Parse the given Table as TableMetadata
362
     *
363
     * @return Mapping\TableMetadata
364
     */
365
    private function convertTableAnnotationToTableMetadata(Annotation\Table $tableAnnot)
366
    {
367
        $table = new Mapping\TableMetadata();
368
369
        if (! empty($tableAnnot->name)) {
370
            $table->setName($tableAnnot->name);
371
        }
372
373
        if (! empty($tableAnnot->schema)) {
374
            $table->setSchema($tableAnnot->schema);
375
        }
376
377
        foreach ($tableAnnot->options as $optionName => $optionValue) {
378
            $table->addOption($optionName, $optionValue);
379
        }
380
381
        foreach ($tableAnnot->indexes as $indexAnnot) {
382
            $table->addIndex([
383
                'name'    => $indexAnnot->name,
384
                'columns' => $indexAnnot->columns,
385
                'unique'  => $indexAnnot->unique,
386
                'options' => $indexAnnot->options,
387
                'flags'   => $indexAnnot->flags,
388
            ]);
389
        }
390
391
        foreach ($tableAnnot->uniqueConstraints as $uniqueConstraintAnnot) {
392
            $table->addUniqueConstraint([
393
                'name'    => $uniqueConstraintAnnot->name,
394
                'columns' => $uniqueConstraintAnnot->columns,
395
                'options' => $uniqueConstraintAnnot->options,
396
                'flags'   => $uniqueConstraintAnnot->flags,
397
            ]);
398
        }
399
400
        return $table;
401
    }
402
403
    /**
404
     * Parse the given Cache as CacheMetadata
405
     *
406
     * @param string|null $fieldName
407
     *
408
     * @return Mapping\CacheMetadata
409
     */
410
    private function convertCacheAnnotationToCacheMetadata(
411
        Annotation\Cache $cacheAnnot,
412
        Mapping\ClassMetadata $metadata,
413
        $fieldName = null
414
    ) {
415
        $usage         = constant(sprintf('%s::%s', Mapping\CacheUsage::class, $cacheAnnot->usage));
416
        $baseRegion    = strtolower(str_replace('\\', '_', $metadata->getRootClassName()));
417
        $defaultRegion = $baseRegion . ($fieldName ? '__' . $fieldName : '');
418
419
        return new Mapping\CacheMetadata($usage, $cacheAnnot->region ?: $defaultRegion);
420
    }
421
422
    /**
423
     * @param Annotation\Annotation[] $propertyAnnotations
424
     *
425
     * @return Mapping\Property
426
     *
427
     * @throws Mapping\MappingException
428
     */
429
    private function convertReflectionPropertyAnnotationsToProperty(
430
        ReflectionProperty $reflectionProperty,
431
        array $propertyAnnotations,
432
        Mapping\ClassMetadata $classMetadata
433
    ) {
434
        // Field can only be annotated with one of:
435
        // @Column, @OneToOne, @OneToMany, @ManyToOne, @ManyToMany, @Embedded
436
        switch (true) {
437
            case isset($propertyAnnotations[Annotation\Column::class]):
438
                return $this->convertReflectionPropertyToFieldMetadata(
439
                    $reflectionProperty,
440
                    $propertyAnnotations,
441
                    $classMetadata
442
                );
443
            case isset($propertyAnnotations[Annotation\OneToOne::class]):
444
                return $this->convertReflectionPropertyToOneToOneAssociationMetadata(
445
                    $reflectionProperty,
446
                    $propertyAnnotations,
447
                    $classMetadata
448
                );
449
            case isset($propertyAnnotations[Annotation\ManyToOne::class]):
450
                return $this->convertReflectionPropertyToManyToOneAssociationMetadata(
451
                    $reflectionProperty,
452
                    $propertyAnnotations,
453
                    $classMetadata
454
                );
455
            case isset($propertyAnnotations[Annotation\OneToMany::class]):
456
                return $this->convertReflectionPropertyToOneToManyAssociationMetadata(
457
                    $reflectionProperty,
458
                    $propertyAnnotations,
459
                    $classMetadata
460
                );
461
            case isset($propertyAnnotations[Annotation\ManyToMany::class]):
462
                return $this->convertReflectionPropertyToManyToManyAssociationMetadata(
463
                    $reflectionProperty,
464
                    $propertyAnnotations,
465
                    $classMetadata
466
                );
467
            case isset($propertyAnnotations[Annotation\Embedded::class]):
468
                // @todo guilhermeblanco Implement later... =)
469
                break;
470
471
            default:
472
                return new Mapping\TransientMetadata($reflectionProperty->getName());
473
        }
474
    }
475
476
    /**
477
     * @param Annotation\Annotation[] $propertyAnnotations
478
     *
479
     * @return Mapping\FieldMetadata
480
     *
481
     * @throws Mapping\MappingException
482
     */
483
    private function convertReflectionPropertyToFieldMetadata(
484
        ReflectionProperty $reflectionProperty,
485
        array $propertyAnnotations,
486
        Mapping\ClassMetadata $classMetadata
487
    ) {
488
        $className    = $classMetadata->getClassName();
489
        $fieldName    = $reflectionProperty->getName();
490
        $columnAnnot  = $propertyAnnotations[Annotation\Column::class];
491
        $isVersioned  = isset($propertyAnnotations[Annotation\Version::class]);
492
        $isPrimaryKey = isset($propertyAnnotations[Annotation\Id::class]);
493
494
        if ($columnAnnot->type === null) {
495
            throw Mapping\MappingException::propertyTypeIsRequired($className, $fieldName);
496
        }
497
498
        if ($isVersioned && $isPrimaryKey) {
499
            throw Mapping\MappingException::cannotVersionIdField($className, $fieldName);
500
        }
501
502
        $columnName = empty($columnAnnot->name)
503
            ? $this->namingStrategy->propertyToColumnName($fieldName, $className)
504
            : $columnAnnot->name;
505
506
        $fieldMetadata = $isVersioned
507
            ? new Mapping\VersionFieldMetadata($fieldName)
508
            : new Mapping\FieldMetadata($fieldName);
509
510
        $fieldMetadata->setType(Type::getType($columnAnnot->type));
511
        $fieldMetadata->setColumnName($columnName);
512
        $fieldMetadata->setScale($columnAnnot->scale);
513
        $fieldMetadata->setPrecision($columnAnnot->precision);
514
        $fieldMetadata->setNullable($columnAnnot->nullable);
515
        $fieldMetadata->setUnique($columnAnnot->unique);
516
517
        // Check for Id
518
        if ($isPrimaryKey) {
519
            if ($fieldMetadata->getType()->canRequireSQLConversion()) {
520
                throw Mapping\MappingException::sqlConversionNotAllowedForPrimaryKeyProperties($className, $fieldMetadata);
521
            }
522
523
            $fieldMetadata->setPrimaryKey(true);
524
        }
525
526
        if (! empty($columnAnnot->columnDefinition)) {
527
            $fieldMetadata->setColumnDefinition($columnAnnot->columnDefinition);
528
        }
529
530
        if (! empty($columnAnnot->length)) {
531
            $fieldMetadata->setLength($columnAnnot->length);
532
        }
533
534
        // Assign default options
535
        $customOptions  = $columnAnnot->options ?? [];
536
        $defaultOptions = [];
537
538
        if ($isVersioned) {
539
            switch ($fieldMetadata->getTypeName()) {
540
                case 'integer':
541
                case 'bigint':
542
                case 'smallint':
543
                    $defaultOptions['default'] = 1;
544
                    break;
545
546
                case 'datetime':
547
                    $defaultOptions['default'] = 'CURRENT_TIMESTAMP';
548
                    break;
549
550
                default:
551
                    if (! isset($customOptions['default'])) {
552
                        throw Mapping\MappingException::unsupportedOptimisticLockingType($fieldMetadata->getType());
553
                    }
554
            }
555
        }
556
557
        $fieldMetadata->setOptions(array_merge($defaultOptions, $customOptions));
558
559
        return $fieldMetadata;
560
    }
561
562
    /**
563
     * @param Annotation\Annotation[] $propertyAnnotations
564
     *
565
     * @return Mapping\OneToOneAssociationMetadata
566
     */
567
    private function convertReflectionPropertyToOneToOneAssociationMetadata(
568
        ReflectionProperty $reflectionProperty,
569
        array $propertyAnnotations,
570
        Mapping\ClassMetadata $classMetadata
571
    ) {
572
        $className     = $classMetadata->getClassName();
573
        $fieldName     = $reflectionProperty->getName();
574
        $oneToOneAnnot = $propertyAnnotations[Annotation\OneToOne::class];
575
576
        if ($oneToOneAnnot->targetEntity === null) {
577
            throw Mapping\MappingException::missingTargetEntity($fieldName);
578
        }
579
580
        $assocMetadata = new Mapping\OneToOneAssociationMetadata($fieldName);
581
        $targetEntity  = $oneToOneAnnot->targetEntity;
582
583
        $assocMetadata->setSourceEntity($className);
584
        $assocMetadata->setTargetEntity($targetEntity);
585
        $assocMetadata->setCascade($this->getCascade($className, $fieldName, $oneToOneAnnot->cascade));
586
        $assocMetadata->setOrphanRemoval($oneToOneAnnot->orphanRemoval);
587
        $assocMetadata->setFetchMode($this->getFetchMode($className, $oneToOneAnnot->fetch));
588
589
        if (! empty($oneToOneAnnot->mappedBy)) {
590
            $assocMetadata->setMappedBy($oneToOneAnnot->mappedBy);
591
        }
592
593
        if (! empty($oneToOneAnnot->inversedBy)) {
594
            $assocMetadata->setInversedBy($oneToOneAnnot->inversedBy);
595
        }
596
597
        // Check for Id
598
        if (isset($propertyAnnotations[Annotation\Id::class])) {
599
            $assocMetadata->setPrimaryKey(true);
600
        }
601
602
        // Check for Cache
603
        if (isset($propertyAnnotations[Annotation\Cache::class])) {
604
            $cacheAnnot    = $propertyAnnotations[Annotation\Cache::class];
605
            $cacheMetadata = $this->convertCacheAnnotationToCacheMetadata($cacheAnnot, $classMetadata, $fieldName);
606
607
            $assocMetadata->setCache($cacheMetadata);
608
        }
609
610
        // Check for JoinColumn/JoinColumns annotations
611
        switch (true) {
612
            case isset($propertyAnnotations[Annotation\JoinColumn::class]):
613
                $joinColumnAnnot = $propertyAnnotations[Annotation\JoinColumn::class];
614
                $joinColumn      = $this->convertJoinColumnAnnotationToJoinColumnMetadata(
615
                    $reflectionProperty,
616
                    $joinColumnAnnot,
617
                    $classMetadata
618
                );
619
620
                $assocMetadata->addJoinColumn($joinColumn);
621
622
                break;
623
624
            case isset($propertyAnnotations[Annotation\JoinColumns::class]):
625
                $joinColumnsAnnot = $propertyAnnotations[Annotation\JoinColumns::class];
626
627
                foreach ($joinColumnsAnnot->value as $joinColumnAnnot) {
628
                    $joinColumn = $this->convertJoinColumnAnnotationToJoinColumnMetadata(
629
                        $reflectionProperty,
630
                        $joinColumnAnnot,
631
                        $classMetadata
632
                    );
633
634
                    $assocMetadata->addJoinColumn($joinColumn);
635
                }
636
637
                break;
638
        }
639
640
        return $assocMetadata;
641
    }
642
643
    /**
644
     * @param Annotation\Annotation[] $propertyAnnotations
645
     *
646
     * @return Mapping\ManyToOneAssociationMetadata
647
     */
648
    private function convertReflectionPropertyToManyToOneAssociationMetadata(
649
        ReflectionProperty $reflectionProperty,
650
        array $propertyAnnotations,
651
        Mapping\ClassMetadata $classMetadata
652
    ) {
653
        $className      = $classMetadata->getClassName();
654
        $fieldName      = $reflectionProperty->getName();
655
        $manyToOneAnnot = $propertyAnnotations[Annotation\ManyToOne::class];
656
657
        if ($manyToOneAnnot->targetEntity === null) {
658
            throw Mapping\MappingException::missingTargetEntity($fieldName);
659
        }
660
661
        $assocMetadata = new Mapping\ManyToOneAssociationMetadata($fieldName);
662
        $targetEntity  = $manyToOneAnnot->targetEntity;
663
664
        $assocMetadata->setSourceEntity($className);
665
        $assocMetadata->setTargetEntity($targetEntity);
666
        $assocMetadata->setCascade($this->getCascade($className, $fieldName, $manyToOneAnnot->cascade));
667
        $assocMetadata->setFetchMode($this->getFetchMode($className, $manyToOneAnnot->fetch));
668
669
        if (! empty($manyToOneAnnot->inversedBy)) {
670
            $assocMetadata->setInversedBy($manyToOneAnnot->inversedBy);
671
        }
672
673
        // Check for Id
674
        if (isset($propertyAnnotations[Annotation\Id::class])) {
675
            $assocMetadata->setPrimaryKey(true);
676
        }
677
678
        // Check for Cache
679
        if (isset($propertyAnnotations[Annotation\Cache::class])) {
680
            $cacheAnnot    = $propertyAnnotations[Annotation\Cache::class];
681
            $cacheMetadata = $this->convertCacheAnnotationToCacheMetadata($cacheAnnot, $classMetadata, $fieldName);
682
683
            $assocMetadata->setCache($cacheMetadata);
684
        }
685
686
        // Check for JoinColumn/JoinColumns annotations
687
        switch (true) {
688
            case isset($propertyAnnotations[Annotation\JoinColumn::class]):
689
                $joinColumnAnnot = $propertyAnnotations[Annotation\JoinColumn::class];
690
                $joinColumn      = $this->convertJoinColumnAnnotationToJoinColumnMetadata(
691
                    $reflectionProperty,
692
                    $joinColumnAnnot,
693
                    $classMetadata
694
                );
695
696
                $assocMetadata->addJoinColumn($joinColumn);
697
698
                break;
699
700
            case isset($propertyAnnotations[Annotation\JoinColumns::class]):
701
                $joinColumnsAnnot = $propertyAnnotations[Annotation\JoinColumns::class];
702
703
                foreach ($joinColumnsAnnot->value as $joinColumnAnnot) {
704
                    $joinColumn = $this->convertJoinColumnAnnotationToJoinColumnMetadata(
705
                        $reflectionProperty,
706
                        $joinColumnAnnot,
707
                        $classMetadata
708
                    );
709
710
                    $assocMetadata->addJoinColumn($joinColumn);
711
                }
712
713
                break;
714
        }
715
716
        return $assocMetadata;
717
    }
718
719
    /**
720
     * @param Annotation\Annotation[] $propertyAnnotations
721
     *
722
     * @return Mapping\OneToManyAssociationMetadata
723
     */
724
    private function convertReflectionPropertyToOneToManyAssociationMetadata(
725
        ReflectionProperty $reflectionProperty,
726
        array $propertyAnnotations,
727
        Mapping\ClassMetadata $classMetadata
728
    ) {
729
        $className      = $classMetadata->getClassName();
730
        $fieldName      = $reflectionProperty->getName();
731
        $oneToManyAnnot = $propertyAnnotations[Annotation\OneToMany::class];
732
733
        if ($oneToManyAnnot->targetEntity === null) {
734
            throw Mapping\MappingException::missingTargetEntity($fieldName);
735
        }
736
737
        $assocMetadata = new Mapping\OneToManyAssociationMetadata($fieldName);
738
        $targetEntity  = $oneToManyAnnot->targetEntity;
739
740
        $assocMetadata->setSourceEntity($className);
741
        $assocMetadata->setTargetEntity($targetEntity);
742
        $assocMetadata->setCascade($this->getCascade($className, $fieldName, $oneToManyAnnot->cascade));
743
        $assocMetadata->setOrphanRemoval($oneToManyAnnot->orphanRemoval);
744
        $assocMetadata->setFetchMode($this->getFetchMode($className, $oneToManyAnnot->fetch));
745
746
        if (! empty($oneToManyAnnot->mappedBy)) {
747
            $assocMetadata->setMappedBy($oneToManyAnnot->mappedBy);
748
        }
749
750
        if (! empty($oneToManyAnnot->indexBy)) {
751
            $assocMetadata->setIndexedBy($oneToManyAnnot->indexBy);
752
        }
753
754
        // Check for OrderBy
755
        if (isset($propertyAnnotations[Annotation\OrderBy::class])) {
756
            $orderByAnnot = $propertyAnnotations[Annotation\OrderBy::class];
757
758
            $assocMetadata->setOrderBy($orderByAnnot->value);
759
        }
760
761
        // Check for Id
762
        if (isset($propertyAnnotations[Annotation\Id::class])) {
763
            $assocMetadata->setPrimaryKey(true);
764
        }
765
766
        // Check for Cache
767
        if (isset($propertyAnnotations[Annotation\Cache::class])) {
768
            $cacheAnnot    = $propertyAnnotations[Annotation\Cache::class];
769
            $cacheMetadata = $this->convertCacheAnnotationToCacheMetadata($cacheAnnot, $classMetadata, $fieldName);
770
771
            $assocMetadata->setCache($cacheMetadata);
772
        }
773
774
        return $assocMetadata;
775
    }
776
777
    /**
778
     * @param Annotation\Annotation[] $propertyAnnotations
779
     *
780
     * @return Mapping\ManyToManyAssociationMetadata
781
     */
782
    private function convertReflectionPropertyToManyToManyAssociationMetadata(
783
        ReflectionProperty $reflectionProperty,
784
        array $propertyAnnotations,
785
        Mapping\ClassMetadata $classMetadata
786
    ) {
787
        $className       = $classMetadata->getClassName();
788
        $fieldName       = $reflectionProperty->getName();
789
        $manyToManyAnnot = $propertyAnnotations[Annotation\ManyToMany::class];
790
791
        if ($manyToManyAnnot->targetEntity === null) {
792
            throw Mapping\MappingException::missingTargetEntity($fieldName);
793
        }
794
795
        $assocMetadata = new Mapping\ManyToManyAssociationMetadata($fieldName);
796
        $targetEntity  = $manyToManyAnnot->targetEntity;
797
798
        $assocMetadata->setSourceEntity($className);
799
        $assocMetadata->setTargetEntity($targetEntity);
800
        $assocMetadata->setCascade($this->getCascade($className, $fieldName, $manyToManyAnnot->cascade));
801
        $assocMetadata->setOrphanRemoval($manyToManyAnnot->orphanRemoval);
802
        $assocMetadata->setFetchMode($this->getFetchMode($className, $manyToManyAnnot->fetch));
803
804
        if (! empty($manyToManyAnnot->mappedBy)) {
805
            $assocMetadata->setMappedBy($manyToManyAnnot->mappedBy);
806
        }
807
808
        if (! empty($manyToManyAnnot->inversedBy)) {
809
            $assocMetadata->setInversedBy($manyToManyAnnot->inversedBy);
810
        }
811
812
        if (! empty($manyToManyAnnot->indexBy)) {
813
            $assocMetadata->setIndexedBy($manyToManyAnnot->indexBy);
814
        }
815
816
        // Check for JoinTable
817
        if (isset($propertyAnnotations[Annotation\JoinTable::class])) {
818
            $joinTableAnnot    = $propertyAnnotations[Annotation\JoinTable::class];
819
            $joinTableMetadata = $this->convertJoinTableAnnotationToJoinTableMetadata(
820
                $reflectionProperty,
821
                $joinTableAnnot,
822
                $classMetadata
823
            );
824
825
            $assocMetadata->setJoinTable($joinTableMetadata);
826
        }
827
828
        // Check for OrderBy
829
        if (isset($propertyAnnotations[Annotation\OrderBy::class])) {
830
            $orderByAnnot = $propertyAnnotations[Annotation\OrderBy::class];
831
832
            $assocMetadata->setOrderBy($orderByAnnot->value);
833
        }
834
835
        // Check for Id
836
        if (isset($propertyAnnotations[Annotation\Id::class])) {
837
            $assocMetadata->setPrimaryKey(true);
838
        }
839
840
        // Check for Cache
841
        if (isset($propertyAnnotations[Annotation\Cache::class])) {
842
            $cacheAnnot    = $propertyAnnotations[Annotation\Cache::class];
843
            $cacheMetadata = $this->convertCacheAnnotationToCacheMetadata($cacheAnnot, $classMetadata, $fieldName);
844
845
            $assocMetadata->setCache($cacheMetadata);
846
        }
847
848
        return $assocMetadata;
849
    }
850
851
    /**
852
     * Parse the given JoinTable as JoinTableMetadata
853
     *
854
     * @return Mapping\JoinTableMetadata
855
     */
856
    private function convertJoinTableAnnotationToJoinTableMetadata(
857
        ReflectionProperty $reflectionProperty,
858
        Annotation\JoinTable $joinTableAnnot,
859
        Mapping\ClassMetadata $classMetadata
860
    ) {
861
        $joinTable = new Mapping\JoinTableMetadata();
862
863
        if (! empty($joinTableAnnot->name)) {
864
            $joinTable->setName($joinTableAnnot->name);
865
        }
866
867
        if (! empty($joinTableAnnot->schema)) {
868
            $joinTable->setSchema($joinTableAnnot->schema);
869
        }
870
871
        foreach ($joinTableAnnot->joinColumns as $joinColumnAnnot) {
872
            $joinColumn = $this->convertJoinColumnAnnotationToJoinColumnMetadata(
873
                $reflectionProperty,
874
                $joinColumnAnnot,
875
                $classMetadata
876
            );
877
878
            $joinTable->addJoinColumn($joinColumn);
879
        }
880
881
        foreach ($joinTableAnnot->inverseJoinColumns as $joinColumnAnnot) {
882
            $joinColumn = $this->convertJoinColumnAnnotationToJoinColumnMetadata(
883
                $reflectionProperty,
884
                $joinColumnAnnot,
885
                $classMetadata
886
            );
887
888
            $joinTable->addInverseJoinColumn($joinColumn);
889
        }
890
891
        return $joinTable;
892
    }
893
894
    /**
895
     * Parse the given JoinColumn as JoinColumnMetadata
896
     *
897
     * @return Mapping\JoinColumnMetadata
898
     */
899
    private function convertJoinColumnAnnotationToJoinColumnMetadata(
900
        ReflectionProperty $reflectionProperty,
901
        Annotation\JoinColumn $joinColumnAnnot,
902
        Mapping\ClassMetadata $classMetadata
903
    ) {
904
        $fieldName            = $reflectionProperty->getName();
905
        $joinColumn           = new Mapping\JoinColumnMetadata();
906
        $columnName           = empty($joinColumnAnnot->name)
907
            ? $this->namingStrategy->propertyToColumnName($fieldName, $classMetadata->getClassName())
908
            : $joinColumnAnnot->name;
909
        $referencedColumnName = empty($joinColumnAnnot->referencedColumnName)
910
            ? $this->namingStrategy->referenceColumnName()
911
            : $joinColumnAnnot->referencedColumnName;
912
913
        $joinColumn->setColumnName($columnName);
914
        $joinColumn->setReferencedColumnName($referencedColumnName);
915
        $joinColumn->setNullable($joinColumnAnnot->nullable);
916
        $joinColumn->setUnique($joinColumnAnnot->unique);
917
918
        if (! empty($joinColumnAnnot->fieldName)) {
919
            $joinColumn->setAliasedName($joinColumnAnnot->fieldName);
920
        }
921
922
        if (! empty($joinColumnAnnot->columnDefinition)) {
923
            $joinColumn->setColumnDefinition($joinColumnAnnot->columnDefinition);
924
        }
925
926
        if ($joinColumnAnnot->onDelete) {
927
            $joinColumn->setOnDelete(strtoupper($joinColumnAnnot->onDelete));
928
        }
929
930
        return $joinColumn;
931
    }
932
933
    /**
934
     * Parses the given method.
935
     *
936
     * @return string[]
937
     */
938
    private function getMethodCallbacks(ReflectionMethod $method)
939
    {
940
        $annotations = $this->getMethodAnnotations($method);
941
        $events      = [
942
            Events::prePersist  => Annotation\PrePersist::class,
943
            Events::postPersist => Annotation\PostPersist::class,
944
            Events::preUpdate   => Annotation\PreUpdate::class,
945
            Events::postUpdate  => Annotation\PostUpdate::class,
946
            Events::preRemove   => Annotation\PreRemove::class,
947
            Events::postRemove  => Annotation\PostRemove::class,
948
            Events::postLoad    => Annotation\PostLoad::class,
949
            Events::preFlush    => Annotation\PreFlush::class,
950
        ];
951
952
        // Check for callbacks
953
        $callbacks = [];
954
955
        foreach ($events as $eventName => $annotationClassName) {
956
            if (isset($annotations[$annotationClassName]) || $method->getName() === $eventName) {
957
                $callbacks[] = $eventName;
958
            }
959
        }
960
961
        return $callbacks;
962
    }
963
964
    /**
965
     * Attempts to resolve the fetch mode.
966
     *
967
     * @param string $className The class name.
968
     * @param string $fetchMode The fetch mode.
969
     *
970
     * @return int The fetch mode as defined in ClassMetadata.
971
     *
972
     * @throws Mapping\MappingException If the fetch mode is not valid.
973
     */
974
    private function getFetchMode($className, $fetchMode)
975
    {
976
        $fetchModeConstant = sprintf('%s::%s', Mapping\FetchMode::class, $fetchMode);
977
978
        if (! defined($fetchModeConstant)) {
979
            throw Mapping\MappingException::invalidFetchMode($className, $fetchMode);
980
        }
981
982
        return constant($fetchModeConstant);
983
    }
984
985
    /**
986
     * @param string   $className        The class name.
987
     * @param string   $fieldName        The field name.
988
     * @param string[] $originalCascades The original unprocessed field cascades.
989
     *
990
     * @return string[] The processed field cascades.
991
     *
992
     * @throws Mapping\MappingException If a cascade option is not valid.
993
     */
994
    private function getCascade(string $className, string $fieldName, array $originalCascades)
995
    {
996
        $cascadeTypes = ['remove', 'persist', 'refresh'];
997
        $cascades     = array_map('strtolower', $originalCascades);
998
999
        if (in_array('all', $cascades, true)) {
1000
            $cascades = $cascadeTypes;
1001
        }
1002
1003
        if (count($cascades) !== count(array_intersect($cascades, $cascadeTypes))) {
1004
            $diffCascades = array_diff($cascades, array_intersect($cascades, $cascadeTypes));
1005
1006
            throw Mapping\MappingException::invalidCascadeOption($diffCascades, $className, $fieldName);
1007
        }
1008
1009
        return $cascades;
1010
    }
1011
1012
    /**
1013
     * @return Annotation\Annotation[]
1014
     */
1015
    private function getClassAnnotations(ReflectionClass $reflectionClass)
1016
    {
1017
        $classAnnotations = $this->reader->getClassAnnotations($reflectionClass);
1018
1019
        foreach ($classAnnotations as $key => $annot) {
1020
            if (! is_numeric($key)) {
1021
                continue;
1022
            }
1023
1024
            $classAnnotations[get_class($annot)] = $annot;
1025
        }
1026
1027
        return $classAnnotations;
1028
    }
1029
1030
    /**
1031
     * @return Annotation\Annotation[]
1032
     */
1033
    private function getPropertyAnnotations(ReflectionProperty $reflectionProperty)
1034
    {
1035
        $propertyAnnotations = $this->reader->getPropertyAnnotations($reflectionProperty);
1036
1037
        foreach ($propertyAnnotations as $key => $annot) {
1038
            if (! is_numeric($key)) {
1039
                continue;
1040
            }
1041
1042
            $propertyAnnotations[get_class($annot)] = $annot;
1043
        }
1044
1045
        return $propertyAnnotations;
1046
    }
1047
1048
    /**
1049
     * @return Annotation\Annotation[]
1050
     */
1051
    private function getMethodAnnotations(ReflectionMethod $reflectionMethod)
1052
    {
1053
        $methodAnnotations = $this->reader->getMethodAnnotations($reflectionMethod);
1054
1055
        foreach ($methodAnnotations as $key => $annot) {
1056
            if (! is_numeric($key)) {
1057
                continue;
1058
            }
1059
1060
            $methodAnnotations[get_class($annot)] = $annot;
1061
        }
1062
1063
        return $methodAnnotations;
1064
    }
1065
}
1066