Passed
Push — master ( b9880b...e1bb9e )
by Guilherme
09:04
created

ClassMetadataFactory::isEntity()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 3
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 6

Importance

Changes 0
Metric Value
cc 2
eloc 1
nc 2
nop 1
dl 0
loc 3
ccs 0
cts 2
cp 0
crap 6
rs 10
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\TableGeneratorNotImplementedYet;
16
use Doctrine\ORM\Mapping\Exception\UnknownGeneratorType;
17
use Doctrine\ORM\Sequencing\Planning\CompositeValueGenerationPlan;
18
use Doctrine\ORM\Sequencing\Planning\NoopValueGenerationPlan;
19
use Doctrine\ORM\Sequencing\Planning\SingleValueGenerationPlan;
20
use Doctrine\ORM\Sequencing\Planning\ValueGenerationExecutor;
21
use InvalidArgumentException;
22
use ReflectionException;
23
use function array_map;
24
use function count;
25
use function in_array;
26
use function reset;
27
use function sprintf;
28
29
/**
30
 * The ClassMetadataFactory is used to create ClassMetadata objects that contain all the
31
 * metadata mapping information of a class which describes how a class should be mapped
32
 * to a relational database.
33
 */
34
class ClassMetadataFactory extends AbstractClassMetadataFactory
35
{
36
    /** @var EntityManagerInterface|null */
37
    private $em;
38
39
    /** @var AbstractPlatform */
40
    private $targetPlatform;
41
42
    /** @var Driver\MappingDriver */
43
    private $driver;
44
45
    /** @var EventManager */
46
    private $evm;
47
48
    /**
49
     * {@inheritdoc}
50
     */
51 387
    protected function loadMetadata(string $name, ClassMetadataBuildingContext $metadataBuildingContext) : array
52
    {
53 387
        $loaded = parent::loadMetadata($name, $metadataBuildingContext);
54
55 363
        array_map([$this, 'resolveDiscriminatorValue'], $loaded);
56
57 363
        return $loaded;
58
    }
59
60 2277
    public function setEntityManager(EntityManagerInterface $em)
61
    {
62 2277
        $this->em = $em;
63 2277
    }
64
65
    /**
66
     * {@inheritdoc}
67
     *
68
     * @throws ORMException
69
     */
70 453
    protected function initialize() : void
71
    {
72 453
        $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

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

88
        $eventArgs = new OnClassMetadataNotFoundEventArgs($className, $metadataBuildingContext, /** @scrutinizer ignore-type */ $this->em);
Loading history...
89
90 2
        $this->evm->dispatchEvent(Events::onClassMetadataNotFound, $eventArgs);
91
92 2
        return $eventArgs->getFoundMetadata();
93
    }
94
95
    /**
96
     * {@inheritdoc}
97
     *
98
     * @throws MappingException
99
     * @throws ORMException
100
     */
101 375
    protected function doLoadMetadata(
102
        string $className,
103
        ?ClassMetadata $parent,
104
        ClassMetadataBuildingContext $metadataBuildingContext
105
    ) : ClassMetadata {
106 375
        $classMetadata = $this->driver->loadMetadataForClass($className, $parent, $metadataBuildingContext);
107
108 369
        $this->completeIdentifierGeneratorMappings($classMetadata);
109 369
        $this->completeRuntimeMetadata($classMetadata, $parent);
110
111 369
        if ($this->evm->hasListeners(Events::loadClassMetadata)) {
112 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

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

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