Completed
Pull Request — master (#7457)
by Michael
11:13
created

createPropertyValueGenerator()   B

Complexity

Conditions 6
Paths 7

Size

Total Lines 33
Code Lines 21

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 17
CRAP Score 6.2488

Importance

Changes 0
Metric Value
cc 6
eloc 21
nc 7
nop 2
dl 0
loc 33
ccs 17
cts 21
cp 0.8095
crap 6.2488
rs 8.9617
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 end;
0 ignored issues
show
introduced by
Type end is not used in this file.
Loading history...
30
use function explode;
0 ignored issues
show
introduced by
Type explode is not used in this file.
Loading history...
31
use function is_subclass_of;
0 ignored issues
show
introduced by
Type is_subclass_of is not used in this file.
Loading history...
32
use function sprintf;
33
use function strpos;
0 ignored issues
show
introduced by
Type strpos is not used in this file.
Loading history...
34
use function strtolower;
0 ignored issues
show
introduced by
Type strtolower is not used in this file.
Loading history...
35
36
/**
37
 * The ClassMetadataFactory is used to create ClassMetadata objects that contain all the
38
 * metadata mapping information of a class which describes how a class should be mapped
39
 * to a relational database.
40
 */
41
class ClassMetadataFactory extends AbstractClassMetadataFactory
42
{
43
    /** @var EntityManagerInterface|null */
44
    private $em;
45
46
    /** @var AbstractPlatform */
47
    private $targetPlatform;
48
49
    /** @var Driver\MappingDriver */
50
    private $driver;
51
52
    /** @var EventManager */
53
    private $evm;
54
55
    /**
56
     * {@inheritdoc}
57
     */
58 385
    protected function loadMetadata(string $name, ClassMetadataBuildingContext $metadataBuildingContext) : array
59
    {
60 385
        $loaded = parent::loadMetadata($name, $metadataBuildingContext);
61
62 363
        array_map([$this, 'resolveDiscriminatorValue'], $loaded);
63
64 363
        return $loaded;
65
    }
66
67 2265
    public function setEntityManager(EntityManagerInterface $em)
68
    {
69 2265
        $this->em = $em;
70 2265
    }
71
72
    /**
73
     * {@inheritdoc}
74
     *
75
     * @throws ORMException
76
     */
77 451
    protected function initialize() : void
78
    {
79 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

79
        $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...
80 451
        $this->evm         = $this->em->getEventManager();
81 451
        $this->initialized = true;
82 451
    }
83
84
    /**
85
     * {@inheritdoc}
86
     */
87 12
    protected function onNotFoundMetadata(
88
        string $className,
89
        ClassMetadataBuildingContext $metadataBuildingContext
90
    ) : ?ClassMetadata {
91 12
        if (! $this->evm->hasListeners(Events::onClassMetadataNotFound)) {
92 10
            return null;
93
        }
94
95 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

95
        $eventArgs = new OnClassMetadataNotFoundEventArgs($className, $metadataBuildingContext, /** @scrutinizer ignore-type */ $this->em);
Loading history...
96
97 2
        $this->evm->dispatchEvent(Events::onClassMetadataNotFound, $eventArgs);
98
99 2
        return $eventArgs->getFoundMetadata();
100
    }
101
102
    /**
103
     * {@inheritdoc}
104
     *
105
     * @throws MappingException
106
     * @throws ORMException
107
     */
108 373
    protected function doLoadMetadata(
109
        string $className,
110
        ?ClassMetadata $parent,
111
        ClassMetadataBuildingContext $metadataBuildingContext
112
    ) : ClassMetadata {
113 373
        $classMetadata = new ClassMetadata($className, $metadataBuildingContext);
114
115 373
        if ($parent) {
116 97
            $classMetadata->setParent($parent);
117
118 97
            foreach ($parent->getDeclaredPropertiesIterator() as $fieldName => $property) {
119 96
                $classMetadata->addInheritedProperty($property);
120
            }
121
122 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

122
            $classMetadata->setInheritanceType(/** @scrutinizer ignore-type */ $parent->inheritanceType);
Loading history...
123 97
            $classMetadata->setIdentifier($parent->identifier);
124
125 97
            if ($parent->discriminatorColumn) {
126 71
                $classMetadata->setDiscriminatorColumn($parent->discriminatorColumn);
127 71
                $classMetadata->setDiscriminatorMap($parent->discriminatorMap);
128
            }
129
130 97
            $classMetadata->setLifecycleCallbacks($parent->lifecycleCallbacks);
131 97
            $classMetadata->setChangeTrackingPolicy($parent->changeTrackingPolicy);
132
133 97
            if ($parent->isMappedSuperclass) {
134 27
                $classMetadata->setCustomRepositoryClassName($parent->getCustomRepositoryClassName());
135
            }
136
        }
137
138
        // Invoke driver
139
        try {
140 373
            $this->driver->loadMetadataForClass($classMetadata->getClassName(), $classMetadata, $metadataBuildingContext);
141 3
        } catch (ReflectionException $e) {
142
            throw MappingException::reflectionFailure($classMetadata->getClassName(), $e);
143
        }
144
145 370
        $this->completeIdentifierGeneratorMappings($classMetadata);
146
147 370
        if ($parent) {
148 97
            if ($parent->getCache()) {
149 3
                $classMetadata->setCache(clone $parent->getCache());
150
            }
151
152 97
            if (! empty($parent->entityListeners) && empty($classMetadata->entityListeners)) {
153 7
                $classMetadata->entityListeners = $parent->entityListeners;
154
            }
155
        }
156
157 370
        $this->completeRuntimeMetadata($classMetadata, $parent);
158
159 370
        if ($this->evm->hasListeners(Events::loadClassMetadata)) {
160 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

160
            $eventArgs = new LoadClassMetadataEventArgs($classMetadata, /** @scrutinizer ignore-type */ $this->em);
Loading history...
161
162 6
            $this->evm->dispatchEvent(Events::loadClassMetadata, $eventArgs);
163
        }
164
165 369
        $this->buildValueGenerationPlan($classMetadata);
166 369
        $this->validateRuntimeMetadata($classMetadata, $parent);
167
168 364
        return $classMetadata;
169
    }
170
171 370
    protected function completeRuntimeMetadata(ClassMetadata $class, ?ClassMetadata $parent = null) : void
172
    {
173 370
        if (! $parent || ! $parent->isMappedSuperclass) {
174 370
            return;
175
        }
176
177 27
        if ($class->isMappedSuperclass) {
178 1
            return;
179
        }
180
181 27
        $tableName = $class->getTableName();
182
183
        // Resolve column table names
184 27
        foreach ($class->getDeclaredPropertiesIterator() as $property) {
185 27
            if ($property instanceof FieldMetadata) {
186 27
                $property->setTableName($property->getTableName() ?? $tableName);
187
188 27
                continue;
189
            }
190
191 12
            if (! ($property instanceof ToOneAssociationMetadata)) {
192 12
                continue;
193
            }
194
195
            // Resolve association join column table names
196 9
            foreach ($property->getJoinColumns() as $joinColumn) {
197
                /** @var JoinColumnMetadata $joinColumn */
198 9
                $joinColumn->setTableName($joinColumn->getTableName() ?? $tableName);
199
            }
200
        }
201 27
    }
202
203
    /**
204
     * Validate runtime metadata is correctly defined.
205
     *
206
     * @throws MappingException
207
     */
208 369
    protected function validateRuntimeMetadata(ClassMetadata $class, ?ClassMetadata $parent = null) : void
209
    {
210 369
        if (! $class->getReflectionClass()) {
211
            // only validate if there is a reflection class instance
212
            return;
213
        }
214
215 369
        $class->validateIdentifier();
216 367
        $class->validateAssociations();
217 367
        $class->validateLifecycleCallbacks($this->getReflectionService());
218
219
        // verify inheritance
220 367
        if (! $class->isMappedSuperclass && $class->inheritanceType !== InheritanceType::NONE) {
221 77
            if (! $parent) {
222 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...
223 3
                    throw MappingException::missingDiscriminatorMap($class->getClassName());
224
                }
225
226 73
                if (! $class->discriminatorColumn) {
227 74
                    throw MappingException::missingDiscriminatorColumn($class->getClassName());
228
                }
229
            }
230 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...
231
            // second condition is necessary for mapped superclasses in the middle of an inheritance hierarchy
232
            throw MappingException::noInheritanceOnMappedSuperClass($class->getClassName());
233
        }
234 364
    }
235
236
    /**
237
     * {@inheritdoc}
238
     */
239 1963
    protected function newClassMetadataBuildingContext() : ClassMetadataBuildingContext
240
    {
241 1963
        return new ClassMetadataBuildingContext(
242 1963
            $this,
243 1963
            $this->getReflectionService(),
244 1963
            $this->em->getConfiguration()->getNamingStrategy()
245
        );
246
    }
247
248
    /**
249
     * Populates the discriminator value of the given metadata (if not set) by iterating over discriminator
250
     * map classes and looking for a fitting one.
251
     *
252
     * @throws \InvalidArgumentException
253
     * @throws \ReflectionException
254
     * @throws MappingException
255
     */
256 363
    private function resolveDiscriminatorValue(ClassMetadata $metadata) : void
257
    {
258 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...
259 363
            ! $metadata->getReflectionClass() || $metadata->getReflectionClass()->isAbstract()) {
260 363
            return;
261
        }
262
263
        // minor optimization: avoid loading related metadata when not needed
264 4
        foreach ($metadata->discriminatorMap as $discriminatorValue => $discriminatorClass) {
265 4
            if ($discriminatorClass === $metadata->getClassName()) {
266 3
                $metadata->discriminatorValue = $discriminatorValue;
267
268 4
                return;
269
            }
270
        }
271
272
        // iterate over discriminator mappings and resolve actual referenced classes according to existing metadata
273 1
        foreach ($metadata->discriminatorMap as $discriminatorValue => $discriminatorClass) {
274 1
            if ($metadata->getClassName() === $this->getMetadataFor($discriminatorClass)->getClassName()) {
275
                $metadata->discriminatorValue = $discriminatorValue;
276
277 1
                return;
278
            }
279
        }
280
281 1
        throw MappingException::mappedClassNotPartOfDiscriminatorMap($metadata->getClassName(), $metadata->getRootClassName());
282
    }
283
284
    /**
285
     * Completes the ID generator mapping. If "auto" is specified we choose the generator
286
     * most appropriate for the targeted database platform.
287
     *
288
     * @throws ORMException
289
     */
290 370
    private function completeIdentifierGeneratorMappings(ClassMetadata $class) : void
291
    {
292 370
        foreach ($class->getDeclaredPropertiesIterator() as $property) {
293 368
            if (! $property instanceof FieldMetadata /*&& ! $property instanceof AssocationMetadata*/) {
294 252
                continue;
295
            }
296
297 366
            $this->completeFieldIdentifierGeneratorMapping($property);
298
        }
299 370
    }
300
301 366
    private function completeFieldIdentifierGeneratorMapping(FieldMetadata $field)
302
    {
303 366
        if (! $field->hasValueGenerator()) {
304 282
            return;
305
        }
306
307 294
        $platform  = $this->getTargetPlatform();
308 294
        $class     = $field->getDeclaringClass();
0 ignored issues
show
Unused Code introduced by
The assignment to $class is dead and can be removed.
Loading history...
309 294
        $generator = $field->getValueGenerator();
310
311 294
        if ($generator->getType() === GeneratorType::AUTO) {
312 284
            $generator = new ValueGeneratorMetadata(
313 284
                $platform->prefersSequences()
314
                    ? GeneratorType::SEQUENCE
315 284
                    : ($platform->prefersIdentityColumns()
316 284
                        ? GeneratorType::IDENTITY
317 284
                        : GeneratorType::TABLE
318
                ),
319 284
                $field->getValueGenerator()->getDefinition()
320
            );
321 284
            $field->setValueGenerator($generator);
322
        }
323
324
        // Validate generator definition and set defaults where needed
325 294
        switch ($generator->getType()) {
326 294
            case GeneratorType::SEQUENCE:
327
                // If there is no sequence definition yet, create a default definition
328 6
                if ($generator->getDefinition()) {
329 6
                    break;
330
                }
331
332
                // @todo guilhermeblanco Move sequence generation to DBAL
333
                $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

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