Failed Conditions
Push — master ( becf73...b9880b )
by Guilherme
09:53
created

ClassMetadataFactory::buildValueGenerationExecutorForProperty()   A

Complexity

Conditions 5
Paths 3

Size

Total Lines 16
Code Lines 7

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 8
CRAP Score 5

Importance

Changes 0
Metric Value
cc 5
eloc 7
nc 3
nop 2
dl 0
loc 16
ccs 8
cts 8
cp 1
crap 5
rs 9.6111
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\Planning\CompositeValueGenerationPlan;
19
use Doctrine\ORM\Sequencing\Planning\NoopValueGenerationPlan;
20
use Doctrine\ORM\Sequencing\Planning\SingleValueGenerationPlan;
21
use Doctrine\ORM\Sequencing\Planning\ValueGenerationExecutor;
22
use InvalidArgumentException;
23
use ReflectionException;
24
use function array_map;
25
use function class_exists;
26
use function count;
27
use function in_array;
28
use function reset;
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 389
    protected function loadMetadata(string $name, ClassMetadataBuildingContext $metadataBuildingContext) : array
54
    {
55 389
        $loaded = parent::loadMetadata($name, $metadataBuildingContext);
56
57 365
        array_map([$this, 'resolveDiscriminatorValue'], $loaded);
58
59 365
        return $loaded;
60
    }
61
62 2279
    public function setEntityManager(EntityManagerInterface $em)
63
    {
64 2279
        $this->em = $em;
65 2279
    }
66
67
    /**
68
     * {@inheritdoc}
69
     *
70
     * @throws ORMException
71
     */
72 455
    protected function initialize() : void
73
    {
74 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

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 455
        $this->evm         = $this->em->getEventManager();
76 455
        $this->initialized = true;
77 455
    }
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 377
    protected function doLoadMetadata(
104
        string $className,
105
        ?ClassMetadata $parent,
106
        ClassMetadataBuildingContext $metadataBuildingContext
107
    ) : ClassMetadata {
108 377
        $classMetadata = $this->driver->loadMetadataForClass($className, $parent, $metadataBuildingContext);
109
110 371
        $this->completeIdentifierGeneratorMappings($classMetadata);
111 371
        $this->completeRuntimeMetadata($classMetadata, $parent);
112
113 371
        if ($this->evm->hasListeners(Events::loadClassMetadata)) {
114 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

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

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