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

ClassMetadataFactory::doLoadMetadata()   A

Complexity

Conditions 6
Paths 10

Size

Total Lines 31
Code Lines 14

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 15
CRAP Score 6

Importance

Changes 0
Metric Value
cc 6
eloc 14
nc 10
nop 3
dl 0
loc 31
ccs 15
cts 15
cp 1
crap 6
rs 9.2222
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;
0 ignored issues
show
introduced by
Type Doctrine\ORM\Sequencing is not used in this file.
Loading history...
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 389
    protected function loadMetadata(string $name, ClassMetadataBuildingContext $metadataBuildingContext) : array
55
    {
56 389
        $loaded = parent::loadMetadata($name, $metadataBuildingContext);
57
58 364
        array_map([$this, 'resolveDiscriminatorValue'], $loaded);
59
60 364
        return $loaded;
61
    }
62
63 2279
    public function setEntityManager(EntityManagerInterface $em)
64
    {
65 2279
        $this->em = $em;
66 2279
    }
67
68
    /**
69
     * {@inheritdoc}
70
     *
71
     * @throws ORMException
72
     */
73 455
    protected function initialize() : void
74
    {
75 455
        $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 455
        $this->evm         = $this->em->getEventManager();
77 455
        $this->initialized = true;
78 455
    }
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 377
    protected function doLoadMetadata(
105
        string $className,
106
        ?ClassMetadata $parent,
107
        ClassMetadataBuildingContext $metadataBuildingContext
108
    ) : ClassMetadata {
109 377
        $classMetadata = $this->driver->loadMetadataForClass($className, $parent, $metadataBuildingContext);
110
111 371
        $this->completeIdentifierGeneratorMappings($classMetadata);
112
113 371
        if ($parent) {
114 96
            if ($parent->getCache()) {
115 3
                $classMetadata->setCache(clone $parent->getCache());
116
            }
117
118 96
            if (! empty($parent->entityListeners) && empty($classMetadata->entityListeners)) {
119 7
                $classMetadata->entityListeners = $parent->entityListeners;
120
            }
121
        }
122
123 371
        $this->completeRuntimeMetadata($classMetadata, $parent);
124
125 371
        if ($this->evm->hasListeners(Events::loadClassMetadata)) {
126 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

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

296
                $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...
297
                $idSequenceName = sprintf('%s_%s_seq', $sequencePrefix, $field->getColumnName());
298
                $sequenceName   = $platform->fixSchemaElementName($idSequenceName);
299
300
                $field->setValueGenerator(
301
                    new ValueGeneratorMetadata(
302
                        $generator->getType(),
303
                        [
304
                            'sequenceName'   => $sequenceName,
305
                            'allocationSize' => 1,
306
                        ]
307
                    )
308
                );
309
310
                break;
311
312 288
            case GeneratorType::TABLE:
313
                throw TableGeneratorNotImplementedYet::create();
314
                break;
315
316 288
            case GeneratorType::CUSTOM:
317 1
                $definition = $generator->getDefinition();
318
319 1
                if (! isset($definition['class'])) {
320
                    throw InvalidCustomGenerator::onClassNotConfigured();
321
                }
322
323 1
                if (! class_exists($definition['class'])) {
324
                    throw InvalidCustomGenerator::onMissingClass($definition);
325
                }
326
327 1
                break;
328
329 287
            case GeneratorType::IDENTITY:
330
            case GeneratorType::NONE:
331 287
                break;
332
333
            default:
334
                throw UnknownGeneratorType::create($generator->getType());
335
        }
336 294
    }
337
338
    /**
339
     * {@inheritDoc}
340
     */
341 198
    protected function getDriver() : Driver\MappingDriver
342
    {
343 198
        return $this->driver;
344
    }
345
346
    /**
347
     * {@inheritDoc}
348
     */
349
    protected function isEntity(ClassMetadata $class) : bool
350
    {
351
        return isset($class->isMappedSuperclass) && $class->isMappedSuperclass === false;
352
    }
353
354 294
    private function getTargetPlatform() : Platforms\AbstractPlatform
355
    {
356 294
        if (! $this->targetPlatform) {
357 294
            $this->targetPlatform = $this->em->getConnection()->getDatabasePlatform();
358
        }
359
360 294
        return $this->targetPlatform;
361
    }
362
363 370
    private function buildValueGenerationPlan(ClassMetadata $class) : void
364
    {
365 370
        $valueGenerationExecutorList = $this->buildValueGenerationExecutorList($class);
366
367 370
        switch (count($valueGenerationExecutorList)) {
368 370
            case 0:
369 92
                $valueGenerationPlan = new NoopValueGenerationPlan();
370 92
                break;
371
372 309
            case 1:
373 304
                $valueGenerationPlan = new SingleValueGenerationPlan($class, $valueGenerationExecutorList[0]);
374 304
                break;
375
376
            default:
377 18
                $valueGenerationPlan = new CompositeValueGenerationPlan($class, $valueGenerationExecutorList);
378 18
                break;
379
        }
380
381 370
        $class->setValueGenerationPlan($valueGenerationPlan);
382 370
    }
383
384
    /**
385
     * @return ValueGenerationExecutor[]
386
     */
387 370
    private function buildValueGenerationExecutorList(ClassMetadata $class) : array
388
    {
389 370
        $executors = [];
390
391 370
        foreach ($class->getDeclaredPropertiesIterator() as $property) {
392 368
            $executor = $this->buildValueGenerationExecutorForProperty($class, $property);
393
394 368
            if ($executor instanceof ValueGenerationExecutor) {
395 309
                $executors[] = $executor;
396
            }
397
        }
398
399 370
        return $executors;
400
    }
401
402 368
    private function buildValueGenerationExecutorForProperty(
403
        ClassMetadata $class,
0 ignored issues
show
Unused Code introduced by
The parameter $class is not used and could be removed. ( Ignorable by Annotation )

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

403
        /** @scrutinizer ignore-unused */ ClassMetadata $class,

This check looks for parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
404
        Property $property
405
    ) : ?ValueGenerationExecutor {
406 368
        if ($property instanceof LocalColumnMetadata && $property->hasValueGenerator()) {
407 293
            return new ColumnValueGeneratorExecutor(
408 293
                $property,
409 293
                $property->getValueGenerator()->getSequencingGenerator($this->getTargetPlatform())
410
            );
411
        }
412
413 334
        if ($property instanceof ToOneAssociationMetadata && $property->isPrimaryKey()) {
414 41
            return new AssociationValueGeneratorExecutor();
415
        }
416
417 330
        return null;
418
    }
419
}
420