Failed Conditions
Push — master ( 4bfa22...148895 )
by Guilherme
17:21 queued 09:56
created

ClassMetadataFactory   F

Complexity

Total Complexity 67

Size/Duplication

Total Lines 480
Duplicated Lines 0 %

Test Coverage

Coverage 91.88%

Importance

Changes 3
Bugs 0 Features 0
Metric Value
eloc 144
dl 0
loc 480
ccs 147
cts 160
cp 0.9188
rs 3.04
c 3
b 0
f 0
wmc 67

23 Methods

Rating   Name   Duplication   Size   Complexity  
A isEntity() 0 3 2
A initialize() 0 5 1
A setReflectionService() 0 3 1
A setEntityManager() 0 3 1
A getTargetPlatform() 0 7 2
A getDriver() 0 3 1
A doLoadMetadata() 0 18 2
A isTransient() 0 9 2
A getLoadedMetadata() 0 3 1
A getParentClasses() 0 12 3
A setCacheDriver() 0 3 1
A getCacheDriver() 0 3 1
A loadMetadata() 0 31 4
A getAllMetadata() 0 14 3
B getMetadataFor() 0 52 9
A setMetadataFor() 0 3 1
A onNotFoundMetadata() 0 13 2
A getReflectionService() 0 7 2
A hasMetadataFor() 0 3 1
A buildValueGenerationPlan() 0 27 5
B validateRuntimeMetadata() 0 24 11
B resolveDiscriminatorValue() 0 26 10
A newClassMetadataBuildingContext() 0 7 1

How to fix   Complexity   

Complex Class

Complex classes like ClassMetadataFactory often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use ClassMetadataFactory, and based on these observations, apply Extract Interface, too.

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

198
        $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...
199 451
        $this->evm         = $this->em->getEventManager();
200 451
        $this->initialized = true;
201 451
    }
202
203
    /**
204
     * {@inheritdoc}
205
     */
206 198
    protected function getDriver() : Driver\MappingDriver
207
    {
208 198
        return $this->driver;
209
    }
210
211 1962
    public function getTargetPlatform() : Platforms\AbstractPlatform
212
    {
213 1962
        if (! $this->targetPlatform) {
214 1962
            $this->targetPlatform = $this->em->getConnection()->getDatabasePlatform();
215
        }
216
217 1962
        return $this->targetPlatform;
218
    }
219
220
    /**
221
     * {@inheritdoc}
222
     */
223
    protected function isEntity(ClassMetadata $class) : bool
224
    {
225
        return isset($class->isMappedSuperclass) && $class->isMappedSuperclass === false;
226
    }
227
228
    /**
229
     * Gets the class metadata descriptor for a class.
230
     *
231
     * @param string $className The name of the class.
232
     *
233
     * @throws InvalidArgumentException
234
     * @throws PersistenceMappingException
235
     * @throws ORMException
236
     */
237 1968
    public function getMetadataFor($className) : ClassMetadata
238
    {
239 1968
        if (isset($this->loadedMetadata[$className])) {
240 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...
241
        }
242
243 1963
        $entityClassName = StaticClassNameConverter::getRealClass($className);
244
245 1963
        if (isset($this->loadedMetadata[$entityClassName])) {
246
            // We do not have the alias name in the map, include it
247 214
            return $this->loadedMetadata[$className] = $this->loadedMetadata[$entityClassName];
248
        }
249
250 1963
        $metadataBuildingContext = $this->newClassMetadataBuildingContext();
251 1963
        $loadingException        = null;
0 ignored issues
show
Unused Code introduced by
The assignment to $loadingException is dead and can be removed.
Loading history...
252
253
        try {
254 1963
            if ($this->cacheDriver) {
255 1859
                $cached = $this->cacheDriver->fetch($entityClassName . $this->cacheSalt);
256
257 1859
                if ($cached instanceof ClassMetadata) {
258 1632
                    $this->loadedMetadata[$entityClassName] = $cached;
259
260 1632
                    $cached->wakeupReflection($metadataBuildingContext->getReflectionService());
261
                } else {
262 1859
                    foreach ($this->loadMetadata($entityClassName, $metadataBuildingContext) as $loadedClass) {
263 267
                        $loadedClassName = $loadedClass->getClassName();
264
265 267
                        $this->cacheDriver->save($loadedClassName . $this->cacheSalt, $loadedClass, null);
266
                    }
267
                }
268
            } else {
269 1955
                $this->loadMetadata($entityClassName, $metadataBuildingContext);
270
            }
271 24
        } catch (PersistenceMappingException $loadingException) {
272 12
            $fallbackMetadataResponse = $this->onNotFoundMetadata($entityClassName, $metadataBuildingContext);
273
274 12
            if (! $fallbackMetadataResponse) {
275 10
                throw $loadingException;
276
            }
277
278 2
            $this->loadedMetadata[$entityClassName] = $fallbackMetadataResponse;
279
        }
280
281 1947
        if ($className !== $entityClassName) {
282
            // We do not have the alias name in the map, include it
283 3
            $this->loadedMetadata[$className] = $this->loadedMetadata[$entityClassName];
284
        }
285
286 1947
        $metadataBuildingContext->validate();
287
288 1947
        return $this->loadedMetadata[$entityClassName];
289
    }
290
291
    /**
292
     * Loads the metadata of the class in question and all it's ancestors whose metadata
293
     * is still not loaded.
294
     *
295
     * Important: The class $name does not necessarily exist at this point here.
296
     * Scenarios in a code-generation setup might have access to XML
297
     * Mapping files without the actual PHP code existing here. That is why the
298
     * {@see Doctrine\ORM\Reflection\ReflectionService} interface
299
     * should be used for reflection.
300
     *
301
     * @param string $name The name of the class for which the metadata should get loaded.
302
     *
303
     * @return ClassMetadata[]
304
     *
305
     * @throws InvalidArgumentException
306
     * @throws ORMException
307
     */
308 385
    protected function loadMetadata(string $name, ClassMetadataBuildingContext $metadataBuildingContext) : array
309
    {
310 385
        if (! $this->initialized) {
311 380
            $this->initialize();
312
        }
313
314 385
        $loaded          = [];
315 385
        $parentClasses   = $this->getParentClasses($name);
316 374
        $parentClasses[] = $name;
317
318
        // Move down the hierarchy of parent classes, starting from the topmost class
319 374
        $parent = null;
320
321 374
        foreach ($parentClasses as $className) {
322 374
            if (isset($this->loadedMetadata[$className])) {
323 71
                $parent = $this->loadedMetadata[$className];
324
325 71
                continue;
326
            }
327
328 374
            $class = $this->doLoadMetadata($className, $parent, $metadataBuildingContext);
329
330 365
            $this->loadedMetadata[$className] = $class;
331
332 365
            $parent   = $class;
333 365
            $loaded[] = $class;
334
        }
335
336 363
        array_map([$this, 'resolveDiscriminatorValue'], $loaded);
337
338 363
        return $loaded;
339
    }
340
341
    /**
342
     * Gets an array of parent classes for the given entity class.
343
     *
344
     * @param string $name
345
     *
346
     * @return string[]
347
     *
348
     * @throws InvalidArgumentException
349
     */
350 385
    protected function getParentClasses($name) : array
351
    {
352
        // Collect parent classes, ignoring transient (not-mapped) classes.
353 385
        $parentClasses = [];
354
355 385
        foreach (array_reverse($this->getReflectionService()->getParentClasses($name)) as $parentClass) {
356 104
            if (! $this->getDriver()->isTransient($parentClass)) {
357 99
                $parentClasses[] = $parentClass;
358
            }
359
        }
360
361 374
        return $parentClasses;
362
    }
363
364
    /**
365
     * {@inheritdoc}
366
     */
367 12
    protected function onNotFoundMetadata(
368
        string $className,
369
        ClassMetadataBuildingContext $metadataBuildingContext
370
    ) : ?ClassMetadata {
371 12
        if (! $this->evm->hasListeners(Events::onClassMetadataNotFound)) {
372 10
            return null;
373
        }
374
375 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

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

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