Failed Conditions
Pull Request — master (#6959)
by Matthew
12:08
created

ClassMetadataFactory::addInheritedProperties()   B

Complexity

Conditions 5
Paths 3

Size

Total Lines 10
Code Lines 5

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 6
CRAP Score 5

Importance

Changes 0
Metric Value
cc 5
eloc 5
nc 3
nop 2
dl 0
loc 10
ccs 6
cts 6
cp 1
crap 5
rs 8.8571
c 0
b 0
f 0
1
<?php
2
3
declare(strict_types=1);
4
5
namespace Doctrine\ORM\Mapping;
6
7
use Doctrine\DBAL\Platforms;
8
use Doctrine\ORM\EntityManagerInterface;
9
use Doctrine\ORM\Event\LoadClassMetadataEventArgs;
10
use Doctrine\ORM\Event\OnClassMetadataNotFoundEventArgs;
11
use Doctrine\ORM\Events;
12
use Doctrine\ORM\ORMException;
13
use Doctrine\ORM\Sequencing;
14
use Doctrine\ORM\Sequencing\Planning\ColumnValueGeneratorExecutor;
15
use Doctrine\ORM\Sequencing\Planning\CompositeValueGenerationPlan;
16
use Doctrine\ORM\Sequencing\Planning\NoopValueGenerationPlan;
17
use Doctrine\ORM\Sequencing\Planning\SingleValueGenerationPlan;
18
use ReflectionException;
19
20
/**
21
 * The ClassMetadataFactory is used to create ClassMetadata objects that contain all the
22
 * metadata mapping information of a class which describes how a class should be mapped
23
 * to a relational database.
24
 */
25
class ClassMetadataFactory extends AbstractClassMetadataFactory
26
{
27
    /**
28
     * @var EntityManagerInterface|null
29
     */
30
    private $em;
31
32
    /**
33
     * @var \Doctrine\DBAL\Platforms\AbstractPlatform
34
     */
35
    private $targetPlatform;
36
37
    /**
38
     * @var Driver\MappingDriver
39
     */
40
    private $driver;
41
42
    /**
43
     * @var \Doctrine\Common\EventManager
44
     */
45
    private $evm;
46
47
    /**
48
     * {@inheritdoc}
49
     */
50 374
    protected function loadMetadata(string $name, ClassMetadataBuildingContext $metadataBuildingContext) : array
51
    {
52 374
        $loaded = parent::loadMetadata($name, $metadataBuildingContext);
53
54 355
        array_map([$this, 'resolveDiscriminatorValue'], $loaded);
55
56 355
        return $loaded;
57
    }
58
59 2252
    public function setEntityManager(EntityManagerInterface $em)
60
    {
61 2252
        $this->em = $em;
62 2252
    }
63
64
    /**
65
     * {@inheritdoc}
66
     *
67
     * @throws ORMException
68
     */
69 440
    protected function initialize() : void
70
    {
71 440
        $this->driver      = $this->em->getConfiguration()->getMetadataDriverImpl();
72 440
        $this->evm         = $this->em->getEventManager();
73 440
        $this->initialized = true;
74 440
    }
75
76
    /**
77
     * {@inheritdoc}
78
     */
79 12
    protected function onNotFoundMetadata(
80
        string $className,
81
        ClassMetadataBuildingContext $metadataBuildingContext
82
    ) : ?ClassMetadata {
83 12
        if (! $this->evm->hasListeners(Events::onClassMetadataNotFound)) {
84 10
            return null;
85
        }
86
87 2
        $eventArgs = new OnClassMetadataNotFoundEventArgs($className, $metadataBuildingContext, $this->em);
88
89 2
        $this->evm->dispatchEvent(Events::onClassMetadataNotFound, $eventArgs);
90
91 2
        return $eventArgs->getFoundMetadata();
92
    }
93
94
    /**
95
     * {@inheritdoc}
96
     *
97
     * @throws MappingException
98
     * @throws ORMException
99
     */
100 362
    protected function doLoadMetadata(
101
        string $className,
102
        ?ClassMetadata $parent,
103
        ClassMetadataBuildingContext $metadataBuildingContext
104
    ) : ClassMetadata {
105 362
        $classMetadata = new ClassMetadata($className, $metadataBuildingContext);
106
107 362
        if ($parent) {
108 98
            $classMetadata->setParent($parent);
109
110 98
            $this->addInheritedProperties($classMetadata, $parent);
111
112 97
            $classMetadata->setInheritanceType($parent->inheritanceType);
113 97
            $classMetadata->setIdentifier($parent->identifier);
114
115 97
            if ($parent->discriminatorColumn) {
116 71
                $classMetadata->setDiscriminatorColumn($parent->discriminatorColumn);
117 71
                $classMetadata->setDiscriminatorMap($parent->discriminatorMap);
118
            }
119
120 97
            $classMetadata->setLifecycleCallbacks($parent->lifecycleCallbacks);
121 97
            $classMetadata->setChangeTrackingPolicy($parent->changeTrackingPolicy);
122
123 97
            if ($parent->isMappedSuperclass) {
124 28
                $classMetadata->setCustomRepositoryClassName($parent->getCustomRepositoryClassName());
125
            }
126
        }
127
128
        // Invoke driver
129
        try {
130 362
            $this->driver->loadMetadataForClass($classMetadata->getClassName(), $classMetadata, $metadataBuildingContext);
131 2
        } catch (ReflectionException $e) {
132
            throw MappingException::reflectionFailure($classMetadata->getClassName(), $e);
133
        }
134
135 360
        $this->completeIdentifierGeneratorMappings($classMetadata);
136
137 360
        if ($parent) {
138 97
            $parentCache = $parent->getCache();
139
140 97
            if ($parentCache) {
141 3
                $classMetadata->setCache(clone $parentCache);
142
            }
143
144 97
            if (! empty($parent->namedNativeQueries)) {
145 7
                $this->addInheritedNamedNativeQueries($classMetadata, $parent);
146
            }
147
148 97
            if (! empty($parent->sqlResultSetMappings)) {
149 7
                $this->addInheritedSqlResultSetMappings($classMetadata, $parent);
150
            }
151
152 97
            if (! empty($parent->entityListeners) && empty($classMetadata->entityListeners)) {
153 7
                $classMetadata->entityListeners = $parent->entityListeners;
154
            }
155
        }
156
157 360
        if (! $classMetadata->discriminatorMap && $classMetadata->inheritanceType !== InheritanceType::NONE && $classMetadata->isRootEntity()) {
158 1
            $this->addDefaultDiscriminatorMap($classMetadata);
159
        }
160
161 360
        $this->completeRuntimeMetadata($classMetadata, $parent);
162
163 360
        if ($this->evm->hasListeners(Events::loadClassMetadata)) {
164 6
            $eventArgs = new LoadClassMetadataEventArgs($classMetadata, $this->em);
165
166 6
            $this->evm->dispatchEvent(Events::loadClassMetadata, $eventArgs);
167
        }
168
169 359
        $this->buildValueGenerationPlan($classMetadata);
170 359
        $this->validateRuntimeMetadata($classMetadata, $parent);
171
172 357
        return $classMetadata;
173
    }
174
175 360
    protected function completeRuntimeMetadata(ClassMetadata $class, ?ClassMetadata $parent = null) : void
176
    {
177 360
        if (! $parent || ! $parent->isMappedSuperclass) {
178 360
            return;
179
        }
180
181 28
        if ($class->isMappedSuperclass) {
182 1
            return;
183
        }
184
185 28
        $tableName = $class->getTableName();
186
187
        // Resolve column table names
188 28
        foreach ($class->getDeclaredPropertiesIterator() as $property) {
189 28
            if ($property instanceof FieldMetadata) {
190 28
                $property->setTableName($property->getTableName() ?? $tableName);
191
192 28
                continue;
193
            }
194
195 12
            if (! ($property instanceof ToOneAssociationMetadata)) {
196 12
                continue;
197
            }
198
199
            // Resolve association join column table names
200 9
            foreach ($property->getJoinColumns() as $joinColumn) {
201
                /** @var JoinColumnMetadata $joinColumn */
202 9
                $joinColumn->setTableName($joinColumn->getTableName() ?? $tableName);
203
            }
204
        }
205 28
    }
206
207
    /**
208
     * Validate runtime metadata is correctly defined.
209
     *
210
     * @throws MappingException
211
     */
212 359
    protected function validateRuntimeMetadata(ClassMetadata $class, ?ClassMetadata $parent = null) : void
213
    {
214 359
        if (! $class->getReflectionClass()) {
215
            // only validate if there is a reflection class instance
216
            return;
217
        }
218
219 359
        $class->validateIdentifier();
220 357
        $class->validateAssociations();
221 357
        $class->validateLifecycleCallbacks($this->getReflectionService());
222
223
        // verify inheritance
224 357
        if (! $class->isMappedSuperclass && $class->inheritanceType !== InheritanceType::NONE) {
225 74
            if (! $parent) {
226 72
                if (! $class->discriminatorMap) {
227
                    throw MappingException::missingDiscriminatorMap($class->getClassName());
228
                }
229
230 72
                if (! $class->discriminatorColumn) {
231 74
                    throw MappingException::missingDiscriminatorColumn($class->getClassName());
232
                }
233
            }
234 322
        } elseif (($class->discriminatorMap || $class->discriminatorColumn) && $class->isMappedSuperclass && $class->isRootEntity()) {
235
            // second condition is necessary for mapped superclasses in the middle of an inheritance hierarchy
236
            throw MappingException::noInheritanceOnMappedSuperClass($class->getClassName());
237
        }
238 357
    }
239
240
    /**
241
     * {@inheritdoc}
242
     */
243 1951
    protected function newClassMetadataBuildingContext() : ClassMetadataBuildingContext
244
    {
245 1951
        return new ClassMetadataBuildingContext(
246 1951
            $this,
247 1951
            $this->getReflectionService(),
248 1951
            $this->em->getConfiguration()->getNamingStrategy()
249
        );
250
    }
251
252
    /**
253
     * Populates the discriminator value of the given metadata (if not set) by iterating over discriminator
254
     * map classes and looking for a fitting one.
255
     *
256
     * @throws \InvalidArgumentException
257
     * @throws \ReflectionException
258
     * @throws MappingException
259
     */
260 355
    private function resolveDiscriminatorValue(ClassMetadata $metadata) : void
261
    {
262 355
        if ($metadata->discriminatorValue || ! $metadata->discriminatorMap || $metadata->isMappedSuperclass ||
263 355
            ! $metadata->getReflectionClass() || $metadata->getReflectionClass()->isAbstract()) {
264 355
            return;
265
        }
266
267
        // minor optimization: avoid loading related metadata when not needed
268 4
        foreach ($metadata->discriminatorMap as $discriminatorValue => $discriminatorClass) {
269 4
            if ($discriminatorClass === $metadata->getClassName()) {
270 3
                $metadata->discriminatorValue = $discriminatorValue;
271
272 4
                return;
273
            }
274
        }
275
276
        // iterate over discriminator mappings and resolve actual referenced classes according to existing metadata
277 1
        foreach ($metadata->discriminatorMap as $discriminatorValue => $discriminatorClass) {
278 1
            if ($metadata->getClassName() === $this->getMetadataFor($discriminatorClass)->getClassName()) {
279
                $metadata->discriminatorValue = $discriminatorValue;
280
281 1
                return;
282
            }
283
        }
284
285 1
        throw MappingException::mappedClassNotPartOfDiscriminatorMap($metadata->getClassName(), $metadata->getRootClassName());
286
    }
287
288
    /**
289
     * Adds a default discriminator map if no one is given
290
     *
291
     * If an entity is of any inheritance type and does not contain a
292
     * discriminator map, then the map is generated automatically. This process
293
     * is expensive computation wise.
294
     *
295
     * The automatically generated discriminator map contains the lowercase short name of
296
     * each class as key.
297
     *
298
     * @throws MappingException
299
     */
300 1
    private function addDefaultDiscriminatorMap(ClassMetadata $class) : void
301
    {
302 1
        $allClasses = $this->driver->getAllClassNames();
303 1
        $fqcn       = $class->getClassName();
304 1
        $map        = [$this->getShortName($fqcn) => $fqcn];
305 1
        $duplicates = [];
306
307 1
        foreach ($allClasses as $subClassCandidate) {
308 1
            if (is_subclass_of($subClassCandidate, $fqcn)) {
309 1
                $shortName = $this->getShortName($subClassCandidate);
310
311 1
                if (isset($map[$shortName])) {
312
                    $duplicates[] = $shortName;
313
                }
314
315 1
                $map[$shortName] = $subClassCandidate;
316
            }
317
        }
318
319 1
        if ($duplicates) {
320
            throw MappingException::duplicateDiscriminatorEntry($class->getClassName(), $duplicates, $map);
321
        }
322
323 1
        $class->setDiscriminatorMap($map);
324 1
    }
325
326
    /**
327
     * Gets the lower-case short name of a class.
328
     *
329
     * @param string $className
330
     */
331 1
    private function getShortName($className) : string
332
    {
333 1
        if (strpos($className, '\\') === false) {
334
            return strtolower($className);
335
        }
336
337 1
        $parts = explode('\\', $className);
338
339 1
        return strtolower(end($parts));
340
    }
341
342
    /**
343
     * Adds inherited fields to the subclass mapping.
344
     *
345
     * @throws MappingException
346
     */
347 98
    private function addInheritedProperties(ClassMetadata $subClass, ClassMetadata $parentClass) : void
348
    {
349 98
        $isAbstract = $parentClass->isMappedSuperclass;
350
351 98
        foreach ($parentClass->getDeclaredPropertiesIterator() as $fieldName => $property) {
352 97
            if ($isAbstract && $property instanceof ToManyAssociationMetadata && ! $property->isOwningSide()) {
353 1
                throw MappingException::illegalToManyAssociationOnMappedSuperclass($parentClass->getClassName(), $fieldName);
354
            }
355
356 96
            $subClass->addInheritedProperty($property);
357
        }
358 97
    }
359
360
    /**
361
     * Adds inherited named native queries to the subclass mapping.
362
     *
363
     * @throws MappingException
364
     */
365 7
    private function addInheritedNamedNativeQueries(ClassMetadata $subClass, ClassMetadata $parentClass) : void
366
    {
367 7
        foreach ($parentClass->namedNativeQueries as $name => $query) {
368 7
            if (isset($subClass->namedNativeQueries[$name])) {
369 4
                continue;
370
            }
371
372 7
            $subClass->addNamedNativeQuery(
373 7
                $name,
374 7
                $query['query'],
375
                [
376 7
                    'resultSetMapping' => $query['resultSetMapping'],
377 7
                    'resultClass'      => $query['resultClass'],
378
                ]
379
            );
380
        }
381 7
    }
382
383
    /**
384
     * Adds inherited sql result set mappings to the subclass mapping.
385
     *
386
     * @throws MappingException
387
     */
388 7
    private function addInheritedSqlResultSetMappings(ClassMetadata $subClass, ClassMetadata $parentClass) : void
389
    {
390 7
        foreach ($parentClass->sqlResultSetMappings as $name => $mapping) {
391 7
            if (isset($subClass->sqlResultSetMappings[$name])) {
392 4
                continue;
393
            }
394
395 7
            $entities = [];
396
397 7
            foreach ($mapping['entities'] as $entity) {
398 7
                $entities[] = [
399 7
                    'fields'              => $entity['fields'],
400 7
                    'discriminatorColumn' => $entity['discriminatorColumn'],
401 7
                    'entityClass'         => $entity['entityClass'],
402
                ];
403
            }
404
405 7
            $subClass->addSqlResultSetMapping(
406
                [
407 7
                    'name'     => $mapping['name'],
408 7
                    'columns'  => $mapping['columns'],
409 7
                    'entities' => $entities,
410
                ]
411
            );
412
        }
413 7
    }
414
415
    /**
416
     * Completes the ID generator mapping. If "auto" is specified we choose the generator
417
     * most appropriate for the targeted database platform.
418
     *
419
     * @throws ORMException
420
     */
421 360
    private function completeIdentifierGeneratorMappings(ClassMetadata $class) : void
422
    {
423 360
        foreach ($class->getDeclaredPropertiesIterator() as $property) {
424 358
            if (! $property instanceof FieldMetadata /*&& ! $property instanceof AssocationMetadata*/) {
0 ignored issues
show
Unused Code Comprehensibility introduced by
45% of this comment could be valid code. Did you maybe forget this after debugging?

Sometimes obsolete code just ends up commented out instead of removed. In this case it is better to remove the code once you have checked you do not need it.

The code might also have been commented out for debugging purposes. In this case it is vital that someone uncomments it again or your project may behave in very unexpected ways in production.

This check looks for comments that seem to be mostly valid code and reports them.

Loading history...
425 249
                continue;
426
            }
427
428 355
            $this->completeFieldIdentifierGeneratorMapping($property);
429
        }
430 360
    }
431
432 355
    private function completeFieldIdentifierGeneratorMapping(FieldMetadata $field)
433
    {
434 355
        if (! $field->hasValueGenerator()) {
435 275
            return;
436
        }
437
438 285
        $platform  = $this->getTargetPlatform();
439 285
        $class     = $field->getDeclaringClass();
440 285
        $generator = $field->getValueGenerator();
441
442 285
        if ($generator->getType() === GeneratorType::AUTO) {
443 274
            $generator = new ValueGeneratorMetadata(
444 274
                $platform->prefersSequences()
445
                    ? GeneratorType::SEQUENCE
446 274
                    : ($platform->prefersIdentityColumns()
447 274
                        ? GeneratorType::IDENTITY
448 274
                        : GeneratorType::TABLE
449
                ),
450 274
                $field->getValueGenerator()->getDefinition()
451
            );
452 274
            $field->setValueGenerator($generator);
453
        }
454
455
        // Validate generator definition and set defaults where needed
456 285
        switch ($generator->getType()) {
457 285
            case GeneratorType::SEQUENCE:
458
                // If there is no sequence definition yet, create a default definition
459 6
                if ($generator->getDefinition()) {
460 6
                    break;
461
                }
462
463
                // @todo guilhermeblanco Move sequence generation to DBAL
464
                $sequencePrefix = $platform->getSequencePrefix($field->getTableName(), $field->getSchemaName());
465
                $idSequenceName = sprintf('%s_%s_seq', $sequencePrefix, $field->getColumnName());
466
                $sequenceName   = $platform->fixSchemaElementName($idSequenceName);
467
468
                $field->setValueGenerator(
469
                    new ValueGeneratorMetadata(
470
                        $generator->getType(),
471
                        [
472
                            'sequenceName'   => $sequenceName,
473
                            'allocationSize' => 1,
474
                        ]
475
                    )
476
                );
477
478
                break;
479
480 279
            case GeneratorType::TABLE:
481
                throw new ORMException('TableGenerator not yet implemented.');
482
                break;
483
484 279
            case GeneratorType::CUSTOM:
485 1
                $definition = $generator->getDefinition();
486 1
                if (! isset($definition['class'])) {
487
                    throw new ORMException(sprintf('Cannot instantiate custom generator, no class has been defined'));
488
                }
489 1
                if (! class_exists($definition['class'])) {
490
                    throw new ORMException(sprintf('Cannot instantiate custom generator : %s', var_export($definition, true))); //$definition['class']));
0 ignored issues
show
Unused Code Comprehensibility introduced by
100% of this comment could be valid code. Did you maybe forget this after debugging?

Sometimes obsolete code just ends up commented out instead of removed. In this case it is better to remove the code once you have checked you do not need it.

The code might also have been commented out for debugging purposes. In this case it is vital that someone uncomments it again or your project may behave in very unexpected ways in production.

This check looks for comments that seem to be mostly valid code and reports them.

Loading history...
491
                }
492
493 1
                break;
494
495 278
            case GeneratorType::IDENTITY:
496
            case GeneratorType::NONE:
497
            case GeneratorType::UUID:
498 278
                break;
499
500
            default:
501
                throw new ORMException('Unknown generator type: ' . $generator->getType());
502
        }
503 285
    }
504
505
    /**
506
     * {@inheritDoc}
507
     */
508 197
    protected function getDriver() : Driver\MappingDriver
509
    {
510 197
        return $this->driver;
511
    }
512
513
    /**
514
     * {@inheritDoc}
515
     */
516
    protected function isEntity(ClassMetadata $class) : bool
517
    {
518
        return isset($class->isMappedSuperclass) && $class->isMappedSuperclass === false;
519
    }
520
521 285
    private function getTargetPlatform() : Platforms\AbstractPlatform
522
    {
523 285
        if (! $this->targetPlatform) {
524 285
            $this->targetPlatform = $this->em->getConnection()->getDatabasePlatform();
525
        }
526
527 285
        return $this->targetPlatform;
528
    }
529
530 359
    private function buildValueGenerationPlan(ClassMetadata $class) : void
531
    {
532
        /** @var LocalColumnMetadata[] $generatedProperties */
533 359
        $generatedProperties = [];
534
535 359
        foreach ($class->getDeclaredPropertiesIterator() as $property) {
536 357
            if (! ($property instanceof LocalColumnMetadata && $property->hasValueGenerator())) {
537 327
                continue;
538
            }
539
540 284
            $generatedProperties[] = $property;
541
        }
542
543 359
        switch (count($generatedProperties)) {
544 359
            case 0:
545 114
                $class->setValueGenerationPlan(new NoopValueGenerationPlan());
546 114
                break;
547
548 284
            case 1:
549 284
                $property = reset($generatedProperties);
550 284
                $executor = new ColumnValueGeneratorExecutor($property, $this->createPropertyValueGenerator($class, $property));
551
552 284
                $class->setValueGenerationPlan(new SingleValueGenerationPlan($class, $executor));
553 284
                break;
554
555
            default:
556
                $executors = [];
557
558
                foreach ($generatedProperties as $property) {
559
                    $executors[] = new ColumnValueGeneratorExecutor($property, $this->createPropertyValueGenerator($class, $property));
560
                }
561
562
                $class->setValueGenerationPlan(new CompositeValueGenerationPlan($class, $executors));
563
                break;
564
        }
565 359
    }
566
567 284
    private function createPropertyValueGenerator(
568
        ClassMetadata $class,
569
        LocalColumnMetadata $property
570
    ) : Sequencing\Generator {
571 284
        $platform = $this->getTargetPlatform();
572
573 284
        switch ($property->getValueGenerator()->getType()) {
574 284
            case GeneratorType::IDENTITY:
575 277
                $sequenceName = null;
576
577
                // Platforms that do not have native IDENTITY support need a sequence to emulate this behaviour.
578 277
                if ($platform->usesSequenceEmulatedIdentityColumns()) {
579
                    $sequencePrefix = $platform->getSequencePrefix($class->getTableName(), $class->getSchemaName());
580
                    $idSequenceName = $platform->getIdentitySequenceName($sequencePrefix, $property->getColumnName());
581
                    $sequenceName   = $platform->quoteIdentifier($platform->fixSchemaElementName($idSequenceName));
582
                }
583
584 277
                return $property->getTypeName() === 'bigint'
585 1
                    ? new Sequencing\BigIntegerIdentityGenerator($sequenceName)
586 277
                    : new Sequencing\IdentityGenerator($sequenceName);
587
588 7
            case GeneratorType::SEQUENCE:
589 6
                $definition = $property->getValueGenerator()->getDefinition();
590 6
                return new Sequencing\SequenceGenerator(
591 6
                    $platform->quoteIdentifier($definition['sequenceName']),
592 6
                    $definition['allocationSize']
593
                );
594
595 1
            case GeneratorType::UUID:
596
                return new Sequencing\UuidGenerator();
597
598 1
            case GeneratorType::CUSTOM:
599 1
                $class = $property->getValueGenerator()->getDefinition()['class'];
600 1
                return new $class();
601
        }
602
603
        throw new \UnexpectedValueException('Platform ' . $property->getValueGenerator()->getType() . ' is not supported');
604
    }
605
}
606