Passed
Pull Request — master (#7457)
by Michael
13:35
created

ClassMetadataFactory::buildValueGenerationPlan()   A

Complexity

Conditions 3
Paths 3

Size

Total Lines 16
Code Lines 11

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 11
CRAP Score 3

Importance

Changes 0
Metric Value
cc 3
eloc 11
nc 3
nop 1
dl 0
loc 16
ccs 11
cts 11
cp 1
crap 3
rs 9.9
c 0
b 0
f 0
1
<?php
2
3
declare(strict_types=1);
4
5
namespace Doctrine\ORM\Mapping;
6
7
use Doctrine\Common\EventManager;
8
use Doctrine\DBAL\Platforms;
9
use Doctrine\DBAL\Platforms\AbstractPlatform;
10
use Doctrine\ORM\EntityManagerInterface;
11
use Doctrine\ORM\Event\LoadClassMetadataEventArgs;
12
use Doctrine\ORM\Event\OnClassMetadataNotFoundEventArgs;
13
use Doctrine\ORM\Events;
14
use Doctrine\ORM\Exception\ORMException;
15
use Doctrine\ORM\Mapping\Exception\InvalidCustomGenerator;
16
use Doctrine\ORM\Mapping\Exception\TableGeneratorNotImplementedYet;
17
use Doctrine\ORM\Mapping\Exception\UnknownGeneratorType;
18
use Doctrine\ORM\Sequencing;
19
use Doctrine\ORM\Sequencing\Planning\AssociationValueGeneratorExecutor;
20
use Doctrine\ORM\Sequencing\Planning\ColumnValueGeneratorExecutor;
21
use Doctrine\ORM\Sequencing\Planning\CompositeValueGenerationPlan;
22
use Doctrine\ORM\Sequencing\Planning\NoopValueGenerationPlan;
23
use Doctrine\ORM\Sequencing\Planning\SingleValueGenerationPlan;
24
use Doctrine\ORM\Sequencing\Planning\ValueGenerationExecutor;
25
use ReflectionException;
26
use function array_map;
27
use function class_exists;
28
use function count;
29
use function sprintf;
30
31
/**
32
 * The ClassMetadataFactory is used to create ClassMetadata objects that contain all the
33
 * metadata mapping information of a class which describes how a class should be mapped
34
 * to a relational database.
35
 */
36
class ClassMetadataFactory extends AbstractClassMetadataFactory
37
{
38
    /** @var EntityManagerInterface|null */
39
    private $em;
40
41
    /** @var AbstractPlatform */
42
    private $targetPlatform;
43
44
    /** @var Driver\MappingDriver */
45
    private $driver;
46
47
    /** @var EventManager */
48
    private $evm;
49
50
    /**
51
     * {@inheritdoc}
52
     */
53 385
    protected function loadMetadata(string $name, ClassMetadataBuildingContext $metadataBuildingContext) : array
54
    {
55 385
        $loaded = parent::loadMetadata($name, $metadataBuildingContext);
56
57 363
        array_map([$this, 'resolveDiscriminatorValue'], $loaded);
58
59 363
        return $loaded;
60
    }
61
62 2265
    public function setEntityManager(EntityManagerInterface $em)
63
    {
64 2265
        $this->em = $em;
65 2265
    }
66
67
    /**
68
     * {@inheritdoc}
69
     *
70
     * @throws ORMException
71
     */
72 451
    protected function initialize() : void
73
    {
74 451
        $this->driver      = $this->em->getConfiguration()->getMetadataDriverImpl();
0 ignored issues
show
Bug introduced by
The method getConfiguration() does not exist on null. ( Ignorable by Annotation )

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

74
        $this->driver      = $this->em->/** @scrutinizer ignore-call */ getConfiguration()->getMetadataDriverImpl();

This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.

This is most likely a typographical error or the method has been renamed.

Loading history...
75 451
        $this->evm         = $this->em->getEventManager();
76 451
        $this->initialized = true;
77 451
    }
78
79
    /**
80
     * {@inheritdoc}
81
     */
82 12
    protected function onNotFoundMetadata(
83
        string $className,
84
        ClassMetadataBuildingContext $metadataBuildingContext
85
    ) : ?ClassMetadata {
86 12
        if (! $this->evm->hasListeners(Events::onClassMetadataNotFound)) {
87 10
            return null;
88
        }
89
90 2
        $eventArgs = new OnClassMetadataNotFoundEventArgs($className, $metadataBuildingContext, $this->em);
0 ignored issues
show
Bug introduced by
It seems like $this->em can also be of type null; however, parameter $entityManager of Doctrine\ORM\Event\OnCla...ventArgs::__construct() does only seem to accept Doctrine\ORM\EntityManagerInterface, maybe add an additional type check? ( Ignorable by Annotation )

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

90
        $eventArgs = new OnClassMetadataNotFoundEventArgs($className, $metadataBuildingContext, /** @scrutinizer ignore-type */ $this->em);
Loading history...
91
92 2
        $this->evm->dispatchEvent(Events::onClassMetadataNotFound, $eventArgs);
93
94 2
        return $eventArgs->getFoundMetadata();
95
    }
96
97
    /**
98
     * {@inheritdoc}
99
     *
100
     * @throws MappingException
101
     * @throws ORMException
102
     */
103 373
    protected function doLoadMetadata(
104
        string $className,
105
        ?ClassMetadata $parent,
106
        ClassMetadataBuildingContext $metadataBuildingContext
107
    ) : ClassMetadata {
108 373
        $classMetadata = new ClassMetadata($className, $metadataBuildingContext);
109
110 373
        if ($parent) {
111 97
            $classMetadata->setParent($parent);
112
113 97
            foreach ($parent->getDeclaredPropertiesIterator() as $fieldName => $property) {
114 96
                $classMetadata->addInheritedProperty($property);
115
            }
116
117 97
            $classMetadata->setInheritanceType($parent->inheritanceType);
0 ignored issues
show
Bug introduced by
$parent->inheritanceType of type string is incompatible with the type integer expected by parameter $type of Doctrine\ORM\Mapping\Cla...a::setInheritanceType(). ( Ignorable by Annotation )

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

117
            $classMetadata->setInheritanceType(/** @scrutinizer ignore-type */ $parent->inheritanceType);
Loading history...
118 97
            $classMetadata->setIdentifier($parent->identifier);
119
120 97
            if ($parent->discriminatorColumn) {
121 71
                $classMetadata->setDiscriminatorColumn($parent->discriminatorColumn);
122 71
                $classMetadata->setDiscriminatorMap($parent->discriminatorMap);
123
            }
124
125 97
            $classMetadata->setLifecycleCallbacks($parent->lifecycleCallbacks);
126 97
            $classMetadata->setChangeTrackingPolicy($parent->changeTrackingPolicy);
127
128 97
            if ($parent->isMappedSuperclass) {
129 27
                $classMetadata->setCustomRepositoryClassName($parent->getCustomRepositoryClassName());
130
            }
131
        }
132
133
        // Invoke driver
134
        try {
135 373
            $this->driver->loadMetadataForClass($classMetadata->getClassName(), $classMetadata, $metadataBuildingContext);
136 3
        } catch (ReflectionException $e) {
137
            throw MappingException::reflectionFailure($classMetadata->getClassName(), $e);
138
        }
139
140 370
        $this->completeIdentifierGeneratorMappings($classMetadata);
141
142 370
        if ($parent) {
143 97
            if ($parent->getCache()) {
144 3
                $classMetadata->setCache(clone $parent->getCache());
145
            }
146
147 97
            if (! empty($parent->entityListeners) && empty($classMetadata->entityListeners)) {
148 7
                $classMetadata->entityListeners = $parent->entityListeners;
149
            }
150
        }
151
152 370
        $this->completeRuntimeMetadata($classMetadata, $parent);
153
154 370
        if ($this->evm->hasListeners(Events::loadClassMetadata)) {
155 6
            $eventArgs = new LoadClassMetadataEventArgs($classMetadata, $this->em);
0 ignored issues
show
Bug introduced by
It seems like $this->em can also be of type null; however, parameter $entityManager of Doctrine\ORM\Event\LoadC...ventArgs::__construct() does only seem to accept Doctrine\ORM\EntityManagerInterface, maybe add an additional type check? ( Ignorable by Annotation )

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

155
            $eventArgs = new LoadClassMetadataEventArgs($classMetadata, /** @scrutinizer ignore-type */ $this->em);
Loading history...
156
157 6
            $this->evm->dispatchEvent(Events::loadClassMetadata, $eventArgs);
158
        }
159
160 369
        $this->buildValueGenerationPlan($classMetadata);
161 369
        $this->validateRuntimeMetadata($classMetadata, $parent);
162
163 364
        return $classMetadata;
164
    }
165
166 370
    protected function completeRuntimeMetadata(ClassMetadata $class, ?ClassMetadata $parent = null) : void
167
    {
168 370
        if (! $parent || ! $parent->isMappedSuperclass) {
169 370
            return;
170
        }
171
172 27
        if ($class->isMappedSuperclass) {
173 1
            return;
174
        }
175
176 27
        $tableName = $class->getTableName();
177
178
        // Resolve column table names
179 27
        foreach ($class->getDeclaredPropertiesIterator() as $property) {
180 27
            if ($property instanceof FieldMetadata) {
181 27
                $property->setTableName($property->getTableName() ?? $tableName);
182
183 27
                continue;
184
            }
185
186 12
            if (! ($property instanceof ToOneAssociationMetadata)) {
187 12
                continue;
188
            }
189
190
            // Resolve association join column table names
191 9
            foreach ($property->getJoinColumns() as $joinColumn) {
192
                /** @var JoinColumnMetadata $joinColumn */
193 9
                $joinColumn->setTableName($joinColumn->getTableName() ?? $tableName);
194
            }
195
        }
196 27
    }
197
198
    /**
199
     * Validate runtime metadata is correctly defined.
200
     *
201
     * @throws MappingException
202
     */
203 369
    protected function validateRuntimeMetadata(ClassMetadata $class, ?ClassMetadata $parent = null) : void
204
    {
205 369
        if (! $class->getReflectionClass()) {
206
            // only validate if there is a reflection class instance
207
            return;
208
        }
209
210 369
        $class->validateIdentifier();
211 367
        $class->validateAssociations();
212 367
        $class->validateLifecycleCallbacks($this->getReflectionService());
213
214
        // verify inheritance
215 367
        if (! $class->isMappedSuperclass && $class->inheritanceType !== InheritanceType::NONE) {
216 77
            if (! $parent) {
217 76
                if (! $class->discriminatorMap) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $class->discriminatorMap of type string[] is implicitly converted to a boolean; are you sure this is intended? If so, consider using empty($expr) instead to make it clear that you intend to check for an array without elements.

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

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

Loading history...
218 3
                    throw MappingException::missingDiscriminatorMap($class->getClassName());
219
                }
220
221 73
                if (! $class->discriminatorColumn) {
222 74
                    throw MappingException::missingDiscriminatorColumn($class->getClassName());
223
                }
224
            }
225 328
        } elseif (($class->discriminatorMap || $class->discriminatorColumn) && $class->isMappedSuperclass && $class->isRootEntity()) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $class->discriminatorMap of type string[] is implicitly converted to a boolean; are you sure this is intended? If so, consider using ! empty($expr) instead to make it clear that you intend to check for an array without elements.

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

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

Loading history...
226
            // second condition is necessary for mapped superclasses in the middle of an inheritance hierarchy
227
            throw MappingException::noInheritanceOnMappedSuperClass($class->getClassName());
228
        }
229 364
    }
230
231
    /**
232
     * {@inheritdoc}
233
     */
234 1963
    protected function newClassMetadataBuildingContext() : ClassMetadataBuildingContext
235
    {
236 1963
        return new ClassMetadataBuildingContext(
237 1963
            $this,
238 1963
            $this->getReflectionService(),
239 1963
            $this->em->getConfiguration()->getNamingStrategy()
240
        );
241
    }
242
243
    /**
244
     * Populates the discriminator value of the given metadata (if not set) by iterating over discriminator
245
     * map classes and looking for a fitting one.
246
     *
247
     * @throws \InvalidArgumentException
248
     * @throws \ReflectionException
249
     * @throws MappingException
250
     */
251 363
    private function resolveDiscriminatorValue(ClassMetadata $metadata) : void
252
    {
253 363
        if ($metadata->discriminatorValue || ! $metadata->discriminatorMap || $metadata->isMappedSuperclass ||
0 ignored issues
show
Bug Best Practice introduced by
The expression $metadata->discriminatorMap of type string[] is implicitly converted to a boolean; are you sure this is intended? If so, consider using empty($expr) instead to make it clear that you intend to check for an array without elements.

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

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

Loading history...
254 363
            ! $metadata->getReflectionClass() || $metadata->getReflectionClass()->isAbstract()) {
255 363
            return;
256
        }
257
258
        // minor optimization: avoid loading related metadata when not needed
259 4
        foreach ($metadata->discriminatorMap as $discriminatorValue => $discriminatorClass) {
260 4
            if ($discriminatorClass === $metadata->getClassName()) {
261 3
                $metadata->discriminatorValue = $discriminatorValue;
262
263 4
                return;
264
            }
265
        }
266
267
        // iterate over discriminator mappings and resolve actual referenced classes according to existing metadata
268 1
        foreach ($metadata->discriminatorMap as $discriminatorValue => $discriminatorClass) {
269 1
            if ($metadata->getClassName() === $this->getMetadataFor($discriminatorClass)->getClassName()) {
270
                $metadata->discriminatorValue = $discriminatorValue;
271
272 1
                return;
273
            }
274
        }
275
276 1
        throw MappingException::mappedClassNotPartOfDiscriminatorMap($metadata->getClassName(), $metadata->getRootClassName());
277
    }
278
279
    /**
280
     * Completes the ID generator mapping. If "auto" is specified we choose the generator
281
     * most appropriate for the targeted database platform.
282
     *
283
     * @throws ORMException
284
     */
285 370
    private function completeIdentifierGeneratorMappings(ClassMetadata $class) : void
286
    {
287 370
        foreach ($class->getDeclaredPropertiesIterator() as $property) {
288 368
            if (! $property instanceof FieldMetadata /*&& ! $property instanceof AssocationMetadata*/) {
289 252
                continue;
290
            }
291
292 366
            $this->completeFieldIdentifierGeneratorMapping($property);
293
        }
294 370
    }
295
296 366
    private function completeFieldIdentifierGeneratorMapping(FieldMetadata $field)
297
    {
298 366
        if (! $field->hasValueGenerator()) {
299 282
            return;
300
        }
301
302 294
        $platform  = $this->getTargetPlatform();
303 294
        $class     = $field->getDeclaringClass();
0 ignored issues
show
Unused Code introduced by
The assignment to $class is dead and can be removed.
Loading history...
304 294
        $generator = $field->getValueGenerator();
305
306 294
        if ($generator->getType() === GeneratorType::AUTO) {
307 284
            $generator = new ValueGeneratorMetadata(
308 284
                $platform->prefersSequences()
309
                    ? GeneratorType::SEQUENCE
310 284
                    : ($platform->prefersIdentityColumns()
311 284
                        ? GeneratorType::IDENTITY
312 284
                        : GeneratorType::TABLE
313
                ),
314 284
                $field->getValueGenerator()->getDefinition()
315
            );
316 284
            $field->setValueGenerator($generator);
317
        }
318
319
        // Validate generator definition and set defaults where needed
320 294
        switch ($generator->getType()) {
321 294
            case GeneratorType::SEQUENCE:
322
                // If there is no sequence definition yet, create a default definition
323 6
                if ($generator->getDefinition()) {
324 6
                    break;
325
                }
326
327
                // @todo guilhermeblanco Move sequence generation to DBAL
328
                $sequencePrefix = $platform->getSequencePrefix($field->getTableName(), $field->getSchemaName());
0 ignored issues
show
Bug introduced by
The method getSchemaName() does not exist on Doctrine\ORM\Mapping\FieldMetadata. ( Ignorable by Annotation )

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

328
                $sequencePrefix = $platform->getSequencePrefix($field->getTableName(), $field->/** @scrutinizer ignore-call */ getSchemaName());

This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.

This is most likely a typographical error or the method has been renamed.

Loading history...
329
                $idSequenceName = sprintf('%s_%s_seq', $sequencePrefix, $field->getColumnName());
330
                $sequenceName   = $platform->fixSchemaElementName($idSequenceName);
331
332
                $field->setValueGenerator(
333
                    new ValueGeneratorMetadata(
334
                        $generator->getType(),
335
                        [
336
                            'sequenceName'   => $sequenceName,
337
                            'allocationSize' => 1,
338
                        ]
339
                    )
340
                );
341
342
                break;
343
344 288
            case GeneratorType::TABLE:
345
                throw TableGeneratorNotImplementedYet::create();
346
                break;
347
348 288
            case GeneratorType::CUSTOM:
349 1
                $definition = $generator->getDefinition();
350 1
                if (! isset($definition['class'])) {
351
                    throw InvalidCustomGenerator::onClassNotConfigured();
352
                }
353 1
                if (! class_exists($definition['class'])) {
354
                    throw InvalidCustomGenerator::onMissingClass($definition);
355
                }
356
357 1
                break;
358
359 287
            case GeneratorType::IDENTITY:
360
            case GeneratorType::NONE:
361 287
                break;
362
363
            default:
364
                throw UnknownGeneratorType::create($generator->getType());
365
        }
366 294
    }
367
368
    /**
369
     * {@inheritDoc}
370
     */
371 199
    protected function getDriver() : Driver\MappingDriver
372
    {
373 199
        return $this->driver;
374
    }
375
376
    /**
377
     * {@inheritDoc}
378
     */
379
    protected function isEntity(ClassMetadata $class) : bool
380
    {
381
        return isset($class->isMappedSuperclass) && $class->isMappedSuperclass === false;
382
    }
383
384 294
    private function getTargetPlatform() : Platforms\AbstractPlatform
385
    {
386 294
        if (! $this->targetPlatform) {
387 294
            $this->targetPlatform = $this->em->getConnection()->getDatabasePlatform();
388
        }
389
390 294
        return $this->targetPlatform;
391
    }
392
393 369
    private function buildValueGenerationPlan(ClassMetadata $class) : void
394
    {
395 369
        $executors = $this->buildValueGenerationExecutorList($class);
396
397 369
        switch (count($executors)) {
398 369
            case 0:
399 91
                $class->setValueGenerationPlan(new NoopValueGenerationPlan());
400 91
                break;
401
402 309
            case 1:
403 304
                $class->setValueGenerationPlan(new SingleValueGenerationPlan($class, $executors[0]));
404 304
                break;
405
406
            default:
407 18
                $class->setValueGenerationPlan(new CompositeValueGenerationPlan($class, $executors));
408 18
                break;
409
        }
410 369
    }
411
412
    /**
413
     * @return ValueGenerationExecutor[]
414
     */
415 369
    private function buildValueGenerationExecutorList(ClassMetadata $class) : array
416
    {
417 369
        $executors = [];
418
419 369
        foreach ($class->getDeclaredPropertiesIterator() as $property) {
420 367
            $executor = $this->buildValueGenerationExecutorForProperty($class, $property);
421
422 367
            if ($executor instanceof ValueGenerationExecutor) {
423 367
                $executors[] = $executor;
424
            }
425
        }
426
427 369
        return $executors;
428
    }
429
430 367
    private function buildValueGenerationExecutorForProperty(
431
        ClassMetadata $class,
432
        Property $property
433
    ) : ?ValueGenerationExecutor {
434 367
        if ($property instanceof LocalColumnMetadata && $property->hasValueGenerator()) {
435 293
            return new ColumnValueGeneratorExecutor($property, $this->createPropertyValueGenerator($class, $property));
436
        }
437
438 334
        if ($property instanceof ToOneAssociationMetadata && $property->isPrimaryKey()) {
439 41
            return new AssociationValueGeneratorExecutor();
440
        }
441
442 330
        return null;
443
    }
444
445 293
    private function createPropertyValueGenerator(
446
        ClassMetadata $class,
447
        LocalColumnMetadata $property
448
    ) : Sequencing\Generator {
449 293
        $platform = $this->getTargetPlatform();
450
451 293
        switch ($property->getValueGenerator()->getType()) {
452 293
            case GeneratorType::IDENTITY:
453 286
                $sequenceName = null;
454
455
                // Platforms that do not have native IDENTITY support need a sequence to emulate this behaviour.
456 286
                if ($platform->usesSequenceEmulatedIdentityColumns()) {
457
                    $sequencePrefix = $platform->getSequencePrefix($class->getTableName(), $class->getSchemaName());
458
                    $idSequenceName = $platform->getIdentitySequenceName($sequencePrefix, $property->getColumnName());
459
                    $sequenceName   = $platform->quoteIdentifier($platform->fixSchemaElementName($idSequenceName));
460
                }
461
462 286
                return $property->getTypeName() === 'bigint'
463 1
                    ? new Sequencing\BigIntegerIdentityGenerator($sequenceName)
464 286
                    : new Sequencing\IdentityGenerator($sequenceName);
465
466 7
            case GeneratorType::SEQUENCE:
467 6
                $definition = $property->getValueGenerator()->getDefinition();
468 6
                return new Sequencing\SequenceGenerator(
469 6
                    $platform->quoteIdentifier($definition['sequenceName']),
470 6
                    $definition['allocationSize']
471
                );
472
                break;
0 ignored issues
show
Unused Code introduced by
break is not strictly necessary here and could be removed.

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

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

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

Loading history...
473
474 1
            case GeneratorType::CUSTOM:
475 1
                $class = $property->getValueGenerator()->getDefinition()['class'];
476 1
                return new $class();
477
                break;
478
        }
479
    }
480
}
481