Failed Conditions
Push — master ( fa7802...d60694 )
by Guilherme
09:27
created

ClassMetadataFactory::validateRuntimeMetadata()   B

Complexity

Conditions 11
Paths 7

Size

Total Lines 25
Code Lines 13

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 12
CRAP Score 11.353

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 11
eloc 13
c 1
b 0
f 0
nc 7
nop 2
dl 0
loc 25
ccs 12
cts 14
cp 0.8571
crap 11.353
rs 7.3166

How to fix   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\Cache\Cache;
8
use Doctrine\Common\EventManager;
9
use Doctrine\Common\Persistence\Mapping\ClassMetadataFactory as PersistenceClassMetadataFactory;
10
use Doctrine\Common\Persistence\Mapping\MappingException as PersistenceMappingException;
11
use Doctrine\DBAL\Platforms;
12
use Doctrine\DBAL\Platforms\AbstractPlatform;
13
use Doctrine\ORM\EntityManagerInterface;
14
use Doctrine\ORM\Event\LoadClassMetadataEventArgs;
15
use Doctrine\ORM\Event\OnClassMetadataNotFoundEventArgs;
16
use Doctrine\ORM\Events;
17
use Doctrine\ORM\Exception\ORMException;
18
use Doctrine\ORM\Mapping\Exception\TableGeneratorNotImplementedYet;
0 ignored issues
show
introduced by
Type Doctrine\ORM\Mapping\Exception\TableGeneratorNotImplementedYet is not used in this file.
Loading history...
19
use Doctrine\ORM\Mapping\Exception\UnknownGeneratorType;
0 ignored issues
show
introduced by
Type Doctrine\ORM\Mapping\Exception\UnknownGeneratorType is not used in this file.
Loading history...
20
use Doctrine\ORM\Reflection\ReflectionService;
21
use Doctrine\ORM\Reflection\RuntimeReflectionService;
22
use Doctrine\ORM\Sequencing\Planning\CompositeValueGenerationPlan;
23
use Doctrine\ORM\Sequencing\Planning\NoopValueGenerationPlan;
24
use Doctrine\ORM\Sequencing\Planning\SingleValueGenerationPlan;
25
use Doctrine\ORM\Sequencing\Executor\ValueGenerationExecutor;
0 ignored issues
show
Coding Style introduced by
Use statements should be sorted alphabetically. The first wrong one is Doctrine\ORM\Sequencing\Executor\ValueGenerationExecutor.
Loading history...
26
use Doctrine\ORM\Utility\StaticClassNameConverter;
27
use InvalidArgumentException;
28
use ReflectionException;
29
use function array_map;
30
use function array_reverse;
31
use function count;
32
use function in_array;
0 ignored issues
show
introduced by
Type in_array is not used in this file.
Loading history...
33
use function reset;
34
use function sprintf;
0 ignored issues
show
introduced by
Type sprintf 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 implements PersistenceClassMetadataFactory
42
{
43
    /**
44
     * Salt used by specific Object Manager implementation.
45
     *
46
     * @var string
47
     */
48
    protected $cacheSalt = '$CLASSMETADATA';
49
50
    /** @var bool */
51
    protected $initialized = false;
52
53
    /** @var ReflectionService|null */
54
    protected $reflectionService;
55
56
    /** @var EntityManagerInterface|null */
57
    private $em;
58
59
    /** @var AbstractPlatform */
60
    private $targetPlatform;
61
62
    /** @var Driver\MappingDriver */
63
    private $driver;
64
65
    /** @var EventManager */
66
    private $evm;
67
68
    /** @var Cache|null */
69
    private $cacheDriver;
70
71
    /** @var ClassMetadata[] */
72
    private $loadedMetadata = [];
73
74
    /**
75
     * Sets the entity manager used to build ClassMetadata instances.
76
     */
77 2275
    public function setEntityManager(EntityManagerInterface $em)
78
    {
79 2275
        $this->em = $em;
80 2275
    }
81
82
    /**
83
     * Sets the cache driver used by the factory to cache ClassMetadata instances.
84
     */
85 2273
    public function setCacheDriver(?Cache $cacheDriver = null) : void
86
    {
87 2273
        $this->cacheDriver = $cacheDriver;
88 2273
    }
89
90
    /**
91
     * Gets the cache driver used by the factory to cache ClassMetadata instances.
92
     */
93
    public function getCacheDriver() : ?Cache
94
    {
95
        return $this->cacheDriver;
96
    }
97
98
    /**
99
     * Returns an array of all the loaded metadata currently in memory.
100
     *
101
     * @return ClassMetadata[]
102
     */
103
    public function getLoadedMetadata() : array
104
    {
105
        return $this->loadedMetadata;
106
    }
107
108
    /**
109
     * Sets the reflectionService.
110
     */
111
    public function setReflectionService(ReflectionService $reflectionService) : void
112
    {
113
        $this->reflectionService = $reflectionService;
114
    }
115
116
    /**
117
     * Gets the reflection service associated with this metadata factory.
118
     */
119 1963
    public function getReflectionService() : ReflectionService
120
    {
121 1963
        if ($this->reflectionService === null) {
122 1963
            $this->reflectionService = new RuntimeReflectionService();
123
        }
124
125 1963
        return $this->reflectionService;
126
    }
127
128
    /**
129
     * {@inheritDoc}
130
     */
131 48
    public function isTransient($className) : bool
132
    {
133 48
        if (! $this->initialized) {
134 15
            $this->initialize();
135
        }
136
137 48
        $entityClassName = StaticClassNameConverter::getRealClass($className);
138
139 48
        return $this->getDriver()->isTransient($entityClassName);
140
    }
141
142
    /**
143
     * Checks whether the factory has the metadata for a class loaded already.
144
     *
145
     * @param string $className
146
     *
147
     * @return bool TRUE if the metadata of the class in question is already loaded, FALSE otherwise.
148
     */
149 66
    public function hasMetadataFor($className) : bool
150
    {
151 66
        return isset($this->loadedMetadata[$className]);
152
    }
153
154
    /**
155
     * Sets the metadata descriptor for a specific class.
156
     *
157
     * NOTE: This is only useful in very special cases, like when generating proxy classes.
158
     *
159
     * @param string        $className
160
     * @param ClassMetadata $class
161
     */
162 5
    public function setMetadataFor($className, $class) : void
163
    {
164 5
        $this->loadedMetadata[$className] = $class;
165 5
    }
166
167
    /**
168
     * Forces the factory to load the metadata of all classes known to the underlying
169
     * mapping driver.
170
     *
171
     * @return ClassMetadata[] The ClassMetadata instances of all mapped classes.
172
     *
173
     * @throws InvalidArgumentException
174
     * @throws ReflectionException
175
     * @throws MappingException
176
     * @throws PersistenceMappingException
177
     * @throws ORMException
178
     */
179 56
    public function getAllMetadata() : array
180
    {
181 56
        if (! $this->initialized) {
182 56
            $this->initialize();
183
        }
184
185 56
        $driver   = $this->getDriver();
186 56
        $metadata = [];
187
188 56
        foreach ($driver->getAllClassNames() as $className) {
189 55
            $metadata[] = $this->getMetadataFor($className);
190
        }
191
192 56
        return $metadata;
0 ignored issues
show
introduced by
The expression return $metadata returns an array which contains values of type Doctrine\ORM\Mapping\ClassMetadata which are incompatible with the return type Doctrine\Common\Persistence\Mapping\ClassMetadata mandated by Doctrine\Common\Persiste...ctory::getAllMetadata().
Loading history...
193
    }
194
195
    /**
196
     * {@inheritdoc}
197
     *
198
     * @throws ORMException
199
     */
200 451
    protected function initialize() : void
201
    {
202 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

202
        $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...
203 451
        $this->evm         = $this->em->getEventManager();
204 451
        $this->initialized = true;
205 451
    }
206
207
    /**
208
     * {@inheritdoc}
209
     */
210 198
    protected function getDriver() : Driver\MappingDriver
211
    {
212 198
        return $this->driver;
213
    }
214
215 1962
    public function getTargetPlatform() : Platforms\AbstractPlatform
216
    {
217 1962
        if (! $this->targetPlatform) {
218 1962
            $this->targetPlatform = $this->em->getConnection()->getDatabasePlatform();
219
        }
220
221 1962
        return $this->targetPlatform;
222
    }
223
224
    /**
225
     * {@inheritdoc}
226
     */
227
    protected function isEntity(ClassMetadata $class) : bool
228
    {
229
        return isset($class->isMappedSuperclass) && $class->isMappedSuperclass === false;
230
    }
231
232
    /**
233
     * Gets the class metadata descriptor for a class.
234
     *
235
     * @param string $className The name of the class.
236
     *
237
     * @throws InvalidArgumentException
238
     * @throws PersistenceMappingException
239
     * @throws ORMException
240
     */
241 1968
    public function getMetadataFor($className) : ClassMetadata
242
    {
243 1968
        if (isset($this->loadedMetadata[$className])) {
244 1646
            return $this->loadedMetadata[$className];
0 ignored issues
show
Bug Best Practice introduced by
The expression return $this->loadedMetadata[$className] returns the type Doctrine\ORM\Mapping\ClassMetadata which is incompatible with the return type mandated by Doctrine\Common\Persiste...ctory::getMetadataFor() of Doctrine\Common\Persistence\Mapping\ClassMetadata.

In the issue above, the returned value is violating the contract defined by the mentioned interface.

Let's take a look at an example:

interface HasName {
    /** @return string */
    public function getName();
}

class Name {
    public $name;
}

class User implements HasName {
    /** @return string|Name */
    public function getName() {
        return new Name('foo'); // This is a violation of the ``HasName`` interface
                                // which only allows a string value to be returned.
    }
}
Loading history...
245
        }
246
247 1963
        $entityClassName = StaticClassNameConverter::getRealClass($className);
248
249 1963
        if (isset($this->loadedMetadata[$entityClassName])) {
250
            // We do not have the alias name in the map, include it
251 214
            return $this->loadedMetadata[$className] = $this->loadedMetadata[$entityClassName];
252
        }
253
254 1963
        $metadataBuildingContext = $this->newClassMetadataBuildingContext();
255 1963
        $loadingException        = null;
0 ignored issues
show
Unused Code introduced by
The assignment to $loadingException is dead and can be removed.
Loading history...
256
257
        try {
258 1963
            if ($this->cacheDriver) {
259 1859
                $cached = $this->cacheDriver->fetch($entityClassName . $this->cacheSalt);
260
261 1859
                if ($cached instanceof ClassMetadata) {
262 1632
                    $this->loadedMetadata[$entityClassName] = $cached;
263
264 1632
                    $cached->wakeupReflection($metadataBuildingContext->getReflectionService());
265
                } else {
266 1859
                    foreach ($this->loadMetadata($entityClassName, $metadataBuildingContext) as $loadedClass) {
267 267
                        $loadedClassName = $loadedClass->getClassName();
268
269 267
                        $this->cacheDriver->save($loadedClassName . $this->cacheSalt, $loadedClass, null);
270
                    }
271
                }
272
            } else {
273 1955
                $this->loadMetadata($entityClassName, $metadataBuildingContext);
274
            }
275 24
        } catch (PersistenceMappingException $loadingException) {
276 12
            $fallbackMetadataResponse = $this->onNotFoundMetadata($entityClassName, $metadataBuildingContext);
277
278 12
            if (! $fallbackMetadataResponse) {
279 10
                throw $loadingException;
280
            }
281
282 2
            $this->loadedMetadata[$entityClassName] = $fallbackMetadataResponse;
283
        }
284
285 1947
        if ($className !== $entityClassName) {
286
            // We do not have the alias name in the map, include it
287 3
            $this->loadedMetadata[$className] = $this->loadedMetadata[$entityClassName];
288
        }
289
290 1947
        $metadataBuildingContext->validate();
291
292 1947
        return $this->loadedMetadata[$entityClassName];
293
    }
294
295
    /**
296
     * Loads the metadata of the class in question and all it's ancestors whose metadata
297
     * is still not loaded.
298
     *
299
     * Important: The class $name does not necessarily exist at this point here.
300
     * Scenarios in a code-generation setup might have access to XML
301
     * Mapping files without the actual PHP code existing here. That is why the
302
     * {@see Doctrine\ORM\Reflection\ReflectionService} interface
303
     * should be used for reflection.
304
     *
305
     * @param string $name The name of the class for which the metadata should get loaded.
306
     *
307
     * @return ClassMetadata[]
308
     *
309
     * @throws InvalidArgumentException
310
     * @throws ORMException
311
     */
312 385
    protected function loadMetadata(string $name, ClassMetadataBuildingContext $metadataBuildingContext) : array
313
    {
314 385
        if (! $this->initialized) {
315 380
            $this->initialize();
316
        }
317
318 385
        $loaded          = [];
319 385
        $parentClasses   = $this->getParentClasses($name);
320 374
        $parentClasses[] = $name;
321
322
        // Move down the hierarchy of parent classes, starting from the topmost class
323 374
        $parent = null;
324
325 374
        foreach ($parentClasses as $className) {
326 374
            if (isset($this->loadedMetadata[$className])) {
327 71
                $parent = $this->loadedMetadata[$className];
328
329 71
                continue;
330
            }
331
332 374
            $class = $this->doLoadMetadata($className, $parent, $metadataBuildingContext);
333
334 365
            $this->loadedMetadata[$className] = $class;
335
336 365
            $parent   = $class;
337 365
            $loaded[] = $class;
338
        }
339
340 363
        array_map([$this, 'resolveDiscriminatorValue'], $loaded);
341
342 363
        return $loaded;
343
    }
344
345
    /**
346
     * Gets an array of parent classes for the given entity class.
347
     *
348
     * @param string $name
349
     *
350
     * @return string[]
351
     *
352
     * @throws InvalidArgumentException
353
     */
354 385
    protected function getParentClasses($name) : array
355
    {
356
        // Collect parent classes, ignoring transient (not-mapped) classes.
357 385
        $parentClasses = [];
358
359 385
        foreach (array_reverse($this->getReflectionService()->getParentClasses($name)) as $parentClass) {
360 104
            if (! $this->getDriver()->isTransient($parentClass)) {
361 99
                $parentClasses[] = $parentClass;
362
            }
363
        }
364
365 374
        return $parentClasses;
366
    }
367
368
    /**
369
     * {@inheritdoc}
370
     */
371 12
    protected function onNotFoundMetadata(
372
        string $className,
373
        ClassMetadataBuildingContext $metadataBuildingContext
374
    ) : ?ClassMetadata {
375 12
        if (! $this->evm->hasListeners(Events::onClassMetadataNotFound)) {
376 10
            return null;
377
        }
378
379 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

379
        $eventArgs = new OnClassMetadataNotFoundEventArgs($className, $metadataBuildingContext, /** @scrutinizer ignore-type */ $this->em);
Loading history...
380
381 2
        $this->evm->dispatchEvent(Events::onClassMetadataNotFound, $eventArgs);
382
383 2
        return $eventArgs->getFoundMetadata();
384
    }
385
386
    /**
387
     * {@inheritdoc}
388
     *
389
     * @throws MappingException
390
     * @throws ORMException
391
     */
392 373
    protected function doLoadMetadata(
393
        string $className,
394
        ?ClassMetadata $parent,
395
        ClassMetadataBuildingContext $metadataBuildingContext
396
    ) : ?ComponentMetadata {
397
        /** @var ClassMetadata $classMetadata */
398 373
        $classMetadata = $this->driver->loadMetadataForClass($className, $parent, $metadataBuildingContext);
399
400 367
        if ($this->evm->hasListeners(Events::loadClassMetadata)) {
401 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

401
            $eventArgs = new LoadClassMetadataEventArgs($classMetadata, /** @scrutinizer ignore-type */ $this->em);
Loading history...
402
403 6
            $this->evm->dispatchEvent(Events::loadClassMetadata, $eventArgs);
404
        }
405
406 367
        $this->buildValueGenerationPlan($classMetadata);
407 367
        $this->validateRuntimeMetadata($classMetadata, $parent);
408
409 364
        return $classMetadata;
410
    }
411
412
    /**
413
     * Validate runtime metadata is correctly defined.
414
     *
415
     * @throws MappingException
416
     */
417 367
    protected function validateRuntimeMetadata(ClassMetadata $class, ?ClassMetadata $parent = null) : void
418
    {
419 367
        if (! $class->getReflectionClass()) {
420
            // only validate if there is a reflection class instance
421
            return;
422
        }
423
424 367
        $class->validateIdentifier();
425 367
        $class->validateAssociations();
426 367
        $class->validateLifecycleCallbacks($this->getReflectionService());
427
428
        // verify inheritance
429 367
        if (! $class->isMappedSuperclass && $class->inheritanceType !== InheritanceType::NONE) {
430 76
            if (! $parent) {
431 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...
432 3
                    throw MappingException::missingDiscriminatorMap($class->getClassName());
433
                }
434
435 72
                if (! $class->discriminatorColumn) {
436 73
                    throw MappingException::missingDiscriminatorColumn($class->getClassName());
437
                }
438
            }
439 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...
440
            // second condition is necessary for mapped superclasses in the middle of an inheritance hierarchy
441
            throw MappingException::noInheritanceOnMappedSuperClass($class->getClassName());
442
        }
443 364
    }
444
445
    /**
446
     * {@inheritdoc}
447
     */
448 1963
    protected function newClassMetadataBuildingContext() : ClassMetadataBuildingContext
449
    {
450 1963
        return new ClassMetadataBuildingContext(
451 1963
            $this,
452 1963
            $this->getReflectionService(),
453 1963
            $this->getTargetPlatform(),
454 1963
            $this->em->getConfiguration()->getNamingStrategy()
455
        );
456
    }
457
458
    /**
459
     * Populates the discriminator value of the given metadata (if not set) by iterating over discriminator
460
     * map classes and looking for a fitting one.
461
     *
462
     * @throws InvalidArgumentException
463
     * @throws ReflectionException
464
     * @throws MappingException
465
     * @throws PersistenceMappingException
466
     */
467 363
    private function resolveDiscriminatorValue(ClassMetadata $metadata) : void
468
    {
469 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...
470 363
            ! $metadata->getReflectionClass() || $metadata->getReflectionClass()->isAbstract()) {
471 363
            return;
472
        }
473
474
        // minor optimization: avoid loading related metadata when not needed
475 4
        foreach ($metadata->discriminatorMap as $discriminatorValue => $discriminatorClass) {
476 4
            if ($discriminatorClass === $metadata->getClassName()) {
477 3
                $metadata->discriminatorValue = $discriminatorValue;
478
479 3
                return;
480
            }
481
        }
482
483
        // iterate over discriminator mappings and resolve actual referenced classes according to existing metadata
484 1
        foreach ($metadata->discriminatorMap as $discriminatorValue => $discriminatorClass) {
485 1
            if ($metadata->getClassName() === $this->getMetadataFor($discriminatorClass)->getClassName()) {
486
                $metadata->discriminatorValue = $discriminatorValue;
487
488
                return;
489
            }
490
        }
491
492 1
        throw MappingException::mappedClassNotPartOfDiscriminatorMap($metadata->getClassName(), $metadata->getRootClassName());
493
    }
494
495 367
    private function buildValueGenerationPlan(ClassMetadata $class) : void
496
    {
497 367
        $valueGenerationExecutorList = [];
498
499 367
        foreach ($class->getPropertiesIterator() as $property) {
500 367
            $executor = $property->getValueGenerationExecutor($this->getTargetPlatform());
501
502 367
            if ($executor instanceof ValueGenerationExecutor) {
503 308
                $valueGenerationExecutorList[$property->getName()] = $executor;
504
            }
505
        }
506
507 367
        switch (count($valueGenerationExecutorList)) {
508 367
            case 0:
509 90
                $valueGenerationPlan = new NoopValueGenerationPlan();
510 90
                break;
511
512 308
            case 1:
513 303
                $valueGenerationPlan = new SingleValueGenerationPlan($class, reset($valueGenerationExecutorList));
514 303
                break;
515
516
            default:
517 18
                $valueGenerationPlan = new CompositeValueGenerationPlan($class, $valueGenerationExecutorList);
518 18
                break;
519
        }
520
521 367
        $class->setValueGenerationPlan($valueGenerationPlan);
522 367
    }
523
}
524