Passed
Pull Request — master (#7448)
by Ilya
14:31
created

ClassMetadataFactory::doLoadMetadata()   B

Complexity

Conditions 11
Paths 55

Size

Total Lines 61
Code Lines 31

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 30
CRAP Score 11.004

Importance

Changes 0
Metric Value
cc 11
eloc 31
nc 55
nop 3
dl 0
loc 61
ccs 30
cts 31
cp 0.9677
crap 11.004
rs 7.3166
c 0
b 0
f 0

How to fix   Long Method    Complexity   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

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 InvalidArgumentException;
26
use ReflectionException;
27
use function array_map;
28
use function class_exists;
29
use function count;
30
use function sprintf;
31
32
/**
33
 * The ClassMetadataFactory is used to create ClassMetadata objects that contain all the
34
 * metadata mapping information of a class which describes how a class should be mapped
35
 * to a relational database.
36
 */
37
class ClassMetadataFactory extends AbstractClassMetadataFactory
38
{
39
    /** @var EntityManagerInterface|null */
40
    private $em;
41
42
    /** @var AbstractPlatform */
43
    private $targetPlatform;
44
45
    /** @var Driver\MappingDriver */
46
    private $driver;
47
48
    /** @var EventManager */
49
    private $evm;
50
51
    /**
52
     * {@inheritdoc}
53
     */
54 385
    protected function loadMetadata(string $name, ClassMetadataBuildingContext $metadataBuildingContext) : array
55
    {
56 385
        $loaded = parent::loadMetadata($name, $metadataBuildingContext);
57
58 363
        array_map([$this, 'resolveDiscriminatorValue'], $loaded);
59
60 363
        return $loaded;
61
    }
62
63 2266
    public function setEntityManager(EntityManagerInterface $em)
64
    {
65 2266
        $this->em = $em;
66 2266
    }
67
68
    /**
69
     * {@inheritdoc}
70
     *
71
     * @throws ORMException
72
     */
73 451
    protected function initialize() : void
74
    {
75 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

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

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

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

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

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