ClassMetadataFactory   F
last analyzed

Complexity

Total Complexity 68

Size/Duplication

Total Lines 486
Duplicated Lines 0 %

Test Coverage

Coverage 92.07%

Importance

Changes 3
Bugs 0 Features 0
Metric Value
eloc 148
dl 0
loc 486
ccs 151
cts 164
cp 0.9207
rs 2.96
c 3
b 0
f 0
wmc 68

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 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
A doLoadMetadata() 0 24 3
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 2280
    public function setEntityManager(EntityManagerInterface $em)
74
    {
75 2280
        $this->em = $em;
76 2280
    }
77
78
    /**
79
     * Sets the cache driver used by the factory to cache ClassMetadata instances.
80
     */
81 2278
    public function setCacheDriver(?Cache $cacheDriver = null) : void
82
    {
83 2278
        $this->cacheDriver = $cacheDriver;
84 2278
    }
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 1968
    public function getReflectionService() : ReflectionService
116
    {
117 1968
        if ($this->reflectionService === null) {
118 1968
            $this->reflectionService = new RuntimeReflectionService();
119
        }
120
121 1968
        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\Persistence\Mapping\ClassMetadata mandated by Doctrine\Persistence\Map...ctory::getAllMetadata().
Loading history...
189
    }
190
191
    /**
192
     * {@inheritdoc}
193
     *
194
     * @throws ORMException
195
     */
196 452
    protected function initialize() : void
197
    {
198 452
        $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 452
        $this->evm         = $this->em->getEventManager();
200 452
        $this->initialized = true;
201 452
    }
202
203
    /**
204
     * {@inheritdoc}
205
     */
206 199
    protected function getDriver() : Driver\MappingDriver
207
    {
208 199
        return $this->driver;
209
    }
210
211 1967
    public function getTargetPlatform() : Platforms\AbstractPlatform
212
    {
213 1967
        if (! $this->targetPlatform) {
214 1967
            $this->targetPlatform = $this->em->getConnection()->getDatabasePlatform();
215
        }
216
217 1967
        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 1973
    public function getMetadataFor($className) : ClassMetadata
238
    {
239 1973
        if (isset($this->loadedMetadata[$className])) {
240 1651
            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\Persistence\Map...ctory::getMetadataFor() of Doctrine\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 1968
        $entityClassName = StaticClassNameConverter::getRealClass($className);
244
245 1968
        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 1968
        $metadataBuildingContext = $this->newClassMetadataBuildingContext();
251 1968
        $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 1968
            if ($this->cacheDriver) {
255 1864
                $cached = $this->cacheDriver->fetch($entityClassName . $this->cacheSalt);
256
257 1864
                if ($cached instanceof ClassMetadata) {
258 1636
                    $this->loadedMetadata[$entityClassName] = $cached;
259
260 1636
                    $cached->wakeupReflection($metadataBuildingContext->getReflectionService());
261
                } else {
262 1864
                    foreach ($this->loadMetadata($entityClassName, $metadataBuildingContext) as $loadedClass) {
263 268
                        $loadedClassName = $loadedClass->getClassName();
264
265 268
                        $this->cacheDriver->save($loadedClassName . $this->cacheSalt, $loadedClass, null);
266
                    }
267
                }
268
            } else {
269 1960
                $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 1952
        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 1952
        $metadataBuildingContext->validate();
287
288 1952
        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 386
    protected function loadMetadata(string $name, ClassMetadataBuildingContext $metadataBuildingContext) : array
309
    {
310 386
        if (! $this->initialized) {
311 381
            $this->initialize();
312
        }
313
314 386
        $loaded          = [];
315 386
        $parentClasses   = $this->getParentClasses($name);
316 375
        $parentClasses[] = $name;
317
318
        // Move down the hierarchy of parent classes, starting from the topmost class
319 375
        $parent = null;
320
321 375
        foreach ($parentClasses as $className) {
322 375
            if (isset($this->loadedMetadata[$className])) {
323 72
                $parent = $this->loadedMetadata[$className];
324
325 72
                continue;
326
            }
327
328 375
            $class = $this->doLoadMetadata($className, $parent, $metadataBuildingContext);
329
330 366
            $this->loadedMetadata[$className] = $class;
331
332 366
            $parent   = $class;
333 366
            $loaded[] = $class;
334
        }
335
336 364
        array_map([$this, 'resolveDiscriminatorValue'], $loaded);
337
338 364
        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 386
    protected function getParentClasses($name) : array
351
    {
352
        // Collect parent classes, ignoring transient (not-mapped) classes.
353 386
        $parentClasses = [];
354
355 386
        foreach (array_reverse($this->getReflectionService()->getParentClasses($name)) as $parentClass) {
356 105
            if (! $this->getDriver()->isTransient($parentClass)) {
357 100
                $parentClasses[] = $parentClass;
358
            }
359
        }
360
361 375
        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 374
    protected function doLoadMetadata(
389
        string $className,
390
        ?ClassMetadata $parent,
391
        ClassMetadataBuildingContext $metadataBuildingContext
392
    ) : ?ComponentMetadata {
393 374
        $reflectionService = $metadataBuildingContext->getReflectionService();
394 374
        $reflectionClass   = $reflectionService->getClass($className);
395 374
        $className         = $reflectionClass ? $reflectionClass->getName() : $className;
396
397
        /** @var ClassMetadata $classMetadata */
398 374
        $classMetadata = $this->driver->loadMetadataForClass($className, $parent, $metadataBuildingContext);
399
400 368
        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 368
        $classMetadata->wakeupReflection($metadataBuildingContext->getReflectionService());
407
408 368
        $this->buildValueGenerationPlan($classMetadata);
409 368
        $this->validateRuntimeMetadata($classMetadata, $parent);
410
411 365
        return $classMetadata;
412
    }
413
414
    /**
415
     * Validate runtime metadata is correctly defined.
416
     *
417
     * @throws MappingException
418
     */
419 368
    protected function validateRuntimeMetadata(ClassMetadata $class, ?ClassMetadata $parent = null) : void
420
    {
421 368
        if (! $class->getReflectionClass()) {
422
            // only validate if there is a reflection class instance
423
            return;
424
        }
425
426 368
        $class->validateIdentifier();
427 368
        $class->validateLifecycleCallbacks($this->getReflectionService());
428
429
        // verify inheritance
430 368
        if (! $class->isMappedSuperclass && $class->inheritanceType !== InheritanceType::NONE) {
431 77
            if (! $parent) {
432 76
                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...
433 3
                    throw MappingException::missingDiscriminatorMap($class->getClassName());
434
                }
435
436 73
                if (! $class->discriminatorColumn) {
437 74
                    throw MappingException::missingDiscriminatorColumn($class->getClassName());
438
                }
439
            }
440 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...
441
            // second condition is necessary for mapped superclasses in the middle of an inheritance hierarchy
442
            throw MappingException::noInheritanceOnMappedSuperClass($class->getClassName());
443
        }
444 365
    }
445
446
    /**
447
     * {@inheritdoc}
448
     */
449 1968
    protected function newClassMetadataBuildingContext() : ClassMetadataBuildingContext
450
    {
451 1968
        return new ClassMetadataBuildingContext(
452 1968
            $this,
453 1968
            $this->getReflectionService(),
454 1968
            $this->getTargetPlatform(),
455 1968
            $this->em->getConfiguration()->getNamingStrategy()
456
        );
457
    }
458
459
    /**
460
     * Populates the discriminator value of the given metadata (if not set) by iterating over discriminator
461
     * map classes and looking for a fitting one.
462
     *
463
     * @throws InvalidArgumentException
464
     * @throws ReflectionException
465
     * @throws MappingException
466
     * @throws PersistenceMappingException
467
     */
468 364
    private function resolveDiscriminatorValue(ClassMetadata $metadata) : void
469
    {
470 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...
471 364
            ! $metadata->getReflectionClass() || $metadata->getReflectionClass()->isAbstract()) {
472 364
            return;
473
        }
474
475
        // minor optimization: avoid loading related metadata when not needed
476 4
        foreach ($metadata->discriminatorMap as $discriminatorValue => $discriminatorClass) {
477 4
            if ($discriminatorClass === $metadata->getClassName()) {
478 3
                $metadata->discriminatorValue = $discriminatorValue;
479
480 3
                return;
481
            }
482
        }
483
484
        // iterate over discriminator mappings and resolve actual referenced classes according to existing metadata
485 1
        foreach ($metadata->discriminatorMap as $discriminatorValue => $discriminatorClass) {
486 1
            if ($metadata->getClassName() === $this->getMetadataFor($discriminatorClass)->getClassName()) {
487
                $metadata->discriminatorValue = $discriminatorValue;
488
489
                return;
490
            }
491
        }
492
493 1
        throw MappingException::mappedClassNotPartOfDiscriminatorMap($metadata->getClassName(), $metadata->getRootClassName());
494
    }
495
496 368
    private function buildValueGenerationPlan(ClassMetadata $class) : void
497
    {
498 368
        $valueGenerationExecutorList = [];
499
500 368
        foreach ($class->getPropertiesIterator() as $property) {
501 368
            $executor = $property->getValueGenerationExecutor($this->getTargetPlatform());
502
503 368
            if ($executor instanceof ValueGenerationExecutor) {
504 309
                $valueGenerationExecutorList[$property->getName()] = $executor;
505
            }
506
        }
507
508 368
        switch (count($valueGenerationExecutorList)) {
509 368
            case 0:
510 90
                $valueGenerationPlan = new NoopValueGenerationPlan();
511 90
                break;
512
513 309
            case 1:
514 304
                $valueGenerationPlan = new SingleValueGenerationPlan($class, reset($valueGenerationExecutorList));
515 304
                break;
516
517
            default:
518 18
                $valueGenerationPlan = new CompositeValueGenerationPlan($class, $valueGenerationExecutorList);
519 18
                break;
520
        }
521
522 368
        $class->setValueGenerationPlan($valueGenerationPlan);
523 368
    }
524
}
525