Test Setup Failed
Push — develop ( 082d66...6f26e1 )
by Guilherme
63:04
created

AbstractClassMetadataFactory   A

Complexity

Total Complexity 35

Size/Duplication

Total Lines 421
Duplicated Lines 0 %

Coupling/Cohesion

Components 1
Dependencies 8

Importance

Changes 0
Metric Value
wmc 35
lcom 1
cbo 8
dl 0
loc 421
rs 9
c 0
b 0
f 0

22 Methods

Rating   Name   Duplication   Size   Complexity  
A setCacheDriver() 0 4 1
A getCacheDriver() 0 4 1
A getLoadedMetadata() 0 4 1
A getAllMetadata() 0 15 3
initialize() 0 1 ?
getFqcnFromAlias() 0 1 ?
getDriver() 0 1 ?
wakeupReflection() 0 1 ?
initializeReflection() 0 1 ?
isEntity() 0 1 ?
C getMetadataFor() 0 60 10
A hasMetadataFor() 0 4 1
A setMetadataFor() 0 4 1
A getParentClasses() 0 13 3
C loadMetadata() 0 51 7
A onNotFoundMetadata() 0 4 1
doLoadMetadata() 0 5 ?
newClassMetadataInstance() 0 4 ?
newClassMetadataBuildingContext() 0 1 ?
A isTransient() 0 15 3
A setReflectionService() 0 4 1
A getReflectionService() 0 8 2
1
<?php
2
3
declare(strict_types=1);
4
5
namespace Doctrine\ORM\Mapping;
6
7
use Doctrine\Common\Persistence\Mapping\ClassMetadataFactory;
0 ignored issues
show
Bug introduced by
This use statement conflicts with another class in this namespace, Doctrine\ORM\Mapping\ClassMetadataFactory.

Let’s assume that you have a directory layout like this:

.
|-- OtherDir
|   |-- Bar.php
|   `-- Foo.php
`-- SomeDir
    `-- Foo.php

and let’s assume the following content of Bar.php:

// Bar.php
namespace OtherDir;

use SomeDir\Foo; // This now conflicts the class OtherDir\Foo

If both files OtherDir/Foo.php and SomeDir/Foo.php are loaded in the same runtime, you will see a PHP error such as the following:

PHP Fatal error:  Cannot use SomeDir\Foo as Foo because the name is already in use in OtherDir/Foo.php

However, as OtherDir/Foo.php does not necessarily have to be loaded and the error is only triggered if it is loaded before OtherDir/Bar.php, this problem might go unnoticed for a while. In order to prevent this error from surfacing, you must import the namespace with a different alias:

// Bar.php
namespace OtherDir;

use SomeDir\Foo as SomeDirFoo; // There is no conflict anymore.
Loading history...
8
use Doctrine\Common\Persistence\Mapping\MappingException;
0 ignored issues
show
Bug introduced by
This use statement conflicts with another class in this namespace, Doctrine\ORM\Mapping\MappingException.

Let’s assume that you have a directory layout like this:

.
|-- OtherDir
|   |-- Bar.php
|   `-- Foo.php
`-- SomeDir
    `-- Foo.php

and let’s assume the following content of Bar.php:

// Bar.php
namespace OtherDir;

use SomeDir\Foo; // This now conflicts the class OtherDir\Foo

If both files OtherDir/Foo.php and SomeDir/Foo.php are loaded in the same runtime, you will see a PHP error such as the following:

PHP Fatal error:  Cannot use SomeDir\Foo as Foo because the name is already in use in OtherDir/Foo.php

However, as OtherDir/Foo.php does not necessarily have to be loaded and the error is only triggered if it is loaded before OtherDir/Bar.php, this problem might go unnoticed for a while. In order to prevent this error from surfacing, you must import the namespace with a different alias:

// Bar.php
namespace OtherDir;

use SomeDir\Foo as SomeDirFoo; // There is no conflict anymore.
Loading history...
9
use Doctrine\Common\Cache\Cache;
10
use Doctrine\Common\Util\ClassUtils;
11
use Doctrine\ORM\Reflection\ReflectionService;
12
use Doctrine\ORM\Reflection\RuntimeReflectionService;
13
use ReflectionException;
14
15
/**
16
 * The ClassMetadataFactory is used to create ClassMetadata objects that contain all the
17
 * metadata mapping information of a class which describes how a class should be mapped
18
 * to a relational database.
19
 *
20
 * This class was abstracted from the ORM ClassMetadataFactory.
21
 *
22
 * @since  2.2
23
 * @author Benjamin Eberlei <[email protected]>
24
 * @author Guilherme Blanco <[email protected]>
25
 * @author Jonathan Wage <[email protected]>
26
 * @author Roman Borschel <[email protected]>
27
 */
28
abstract class AbstractClassMetadataFactory implements ClassMetadataFactory
29
{
30
    /**
31
     * Salt used by specific Object Manager implementation.
32
     *
33
     * @var string
34
     */
35
    protected $cacheSalt = '$CLASSMETADATA';
36
37
    /**
38
     * @var \Doctrine\Common\Cache\Cache|null
39
     */
40
    private $cacheDriver;
41
42
    /**
43
     * @var ClassMetadata[]
44
     */
45
    private $loadedMetadata = [];
46
47
    /**
48
     * @var bool
49
     */
50
    protected $initialized = false;
51
52
    /**
53
     * @var ReflectionService|null
54
     */
55
    private $reflectionService = null;
56
57
    /**
58
     * Sets the cache driver used by the factory to cache ClassMetadata instances.
59
     *
60
     * @param \Doctrine\Common\Cache\Cache $cacheDriver
61
     *
62
     * @return void
63
     */
64
    public function setCacheDriver(Cache $cacheDriver = null)
65
    {
66
        $this->cacheDriver = $cacheDriver;
67
    }
68
69
    /**
70
     * Gets the cache driver used by the factory to cache ClassMetadata instances.
71
     *
72
     * @return \Doctrine\Common\Cache\Cache|null
73
     */
74
    public function getCacheDriver()
75
    {
76
        return $this->cacheDriver;
77
    }
78
79
    /**
80
     * Returns an array of all the loaded metadata currently in memory.
81
     *
82
     * @return ClassMetadata[]
83
     */
84
    public function getLoadedMetadata()
85
    {
86
        return $this->loadedMetadata;
87
    }
88
89
    /**
90
     * Forces the factory to load the metadata of all classes known to the underlying
91
     * mapping driver.
92
     *
93
     * @return array The ClassMetadata instances of all mapped classes.
94
     */
95
    public function getAllMetadata()
96
    {
97
        if (! $this->initialized) {
98
            $this->initialize();
99
        }
100
101
        $driver = $this->getDriver();
102
        $metadata = [];
103
104
        foreach ($driver->getAllClassNames() as $className) {
105
            $metadata[] = $this->getMetadataFor($className);
106
        }
107
108
        return $metadata;
109
    }
110
111
    /**
112
     * Lazy initialization of this stuff, especially the metadata driver,
113
     * since these are not needed at all when a metadata cache is active.
114
     *
115
     * @return void
116
     */
117
    abstract protected function initialize();
118
119
    /**
120
     * Gets the fully qualified class-name from the namespace alias.
121
     *
122
     * @param string $namespaceAlias
123
     * @param string $simpleClassName
124
     *
125
     * @return string
126
     */
127
    abstract protected function getFqcnFromAlias($namespaceAlias, $simpleClassName);
128
129
    /**
130
     * Returns the mapping driver implementation.
131
     *
132
     * @return Driver\MappingDriver
133
     */
134
    abstract protected function getDriver();
135
136
    /**
137
     * Wakes up reflection after ClassMetadata gets unserialized from cache.
138
     *
139
     * @param ClassMetadata     $class
140
     * @param ReflectionService $reflService
141
     *
142
     * @return void
143
     */
144
    abstract protected function wakeupReflection(ClassMetadata $class, ReflectionService $reflService);
145
146
    /**
147
     * Initializes Reflection after ClassMetadata was constructed.
148
     *
149
     * @param ClassMetadata     $class
150
     * @param ReflectionService $reflService
151
     *
152
     * @return void
153
     */
154
    abstract protected function initializeReflection(ClassMetadata $class, ReflectionService $reflService);
155
156
    /**
157
     * Checks whether the class metadata is an entity.
158
     *
159
     * This method should return false for mapped superclasses or embedded classes.
160
     *
161
     * @param ClassMetadata $class
162
     *
163
     * @return boolean
164
     */
165
    abstract protected function isEntity(ClassMetadata $class);
166
167
    /**
168
     * Gets the class metadata descriptor for a class.
169
     *
170
     * @param string $className The name of the class.
171
     *
172
     * @return ClassMetadata
173
     *
174
     * @throws ReflectionException
175
     * @throws MappingException
176
     */
177
    public function getMetadataFor($className)
178
    {
179
        if (isset($this->loadedMetadata[$className])) {
180
            return $this->loadedMetadata[$className];
0 ignored issues
show
Bug Best Practice introduced by
The return type of return $this->loadedMetadata[$className]; (Doctrine\ORM\Mapping\ClassMetadata) is incompatible with the return type declared by the interface Doctrine\Common\Persiste...Factory::getMetadataFor of type Doctrine\Common\Persistence\Mapping\ClassMetadata.

If you return a value from a function or method, it should be a sub-type of the type that is given by the parent type f.e. an interface, or abstract method. This is more formally defined by the Lizkov substitution principle, and guarantees that classes that depend on the parent type can use any instance of a child type interchangably. This principle also belongs to the SOLID principles for object oriented design.

Let’s take a look at an example:

class Author {
    private $name;

    public function __construct($name) {
        $this->name = $name;
    }

    public function getName() {
        return $this->name;
    }
}

abstract class Post {
    public function getAuthor() {
        return 'Johannes';
    }
}

class BlogPost extends Post {
    public function getAuthor() {
        return new Author('Johannes');
    }
}

class ForumPost extends Post { /* ... */ }

function my_function(Post $post) {
    echo strtoupper($post->getAuthor());
}

Our function my_function expects a Post object, and outputs the author of the post. The base class Post returns a simple string and outputting a simple string will work just fine. However, the child class BlogPost which is a sub-type of Post instead decided to return an object, and is therefore violating the SOLID principles. If a BlogPost were passed to my_function, PHP would not complain, but ultimately fail when executing the strtoupper call in its body.

Loading history...
181
        }
182
183
        // Check for namespace alias
184
        if (strpos($className, ':') !== false) {
185
            list($namespaceAlias, $simpleClassName) = explode(':', $className, 2);
186
187
            $realClassName = $this->getFqcnFromAlias($namespaceAlias, $simpleClassName);
188
        } else {
189
            $realClassName = ClassUtils::getRealClass($className);
190
        }
191
192
        if (isset($this->loadedMetadata[$realClassName])) {
193
            // We do not have the alias name in the map, include it
194
            return $this->loadedMetadata[$className] = $this->loadedMetadata[$realClassName];
0 ignored issues
show
Bug Best Practice introduced by
The return type of return $this->loadedMeta...tadata[$realClassName]; (Doctrine\ORM\Mapping\ClassMetadata) is incompatible with the return type declared by the interface Doctrine\Common\Persiste...Factory::getMetadataFor of type Doctrine\Common\Persistence\Mapping\ClassMetadata.

If you return a value from a function or method, it should be a sub-type of the type that is given by the parent type f.e. an interface, or abstract method. This is more formally defined by the Lizkov substitution principle, and guarantees that classes that depend on the parent type can use any instance of a child type interchangably. This principle also belongs to the SOLID principles for object oriented design.

Let’s take a look at an example:

class Author {
    private $name;

    public function __construct($name) {
        $this->name = $name;
    }

    public function getName() {
        return $this->name;
    }
}

abstract class Post {
    public function getAuthor() {
        return 'Johannes';
    }
}

class BlogPost extends Post {
    public function getAuthor() {
        return new Author('Johannes');
    }
}

class ForumPost extends Post { /* ... */ }

function my_function(Post $post) {
    echo strtoupper($post->getAuthor());
}

Our function my_function expects a Post object, and outputs the author of the post. The base class Post returns a simple string and outputting a simple string will work just fine. However, the child class BlogPost which is a sub-type of Post instead decided to return an object, and is therefore violating the SOLID principles. If a BlogPost were passed to my_function, PHP would not complain, but ultimately fail when executing the strtoupper call in its body.

Loading history...
195
        }
196
197
        $metadataBuildingContext = $this->newClassMetadataBuildingContext();
198
        $loadingException        = null;
199
200
        try {
201
            if ($this->cacheDriver) {
202
                $cached = $this->cacheDriver->fetch($realClassName . $this->cacheSalt);
203
204
                if ($cached instanceof ClassMetadata) {
205
                    $this->loadedMetadata[$realClassName] = $cached;
206
207
                    $this->wakeupReflection($cached, $this->getReflectionService());
208
                } else {
209
                    foreach ($this->loadMetadata($realClassName, $metadataBuildingContext) as $loadedClassName) {
210
                        $this->cacheDriver->save(
211
                            $loadedClassName . $this->cacheSalt,
212
                            $this->loadedMetadata[$loadedClassName],
213
                            null
214
                        );
215
                    }
216
                }
217
            } else {
218
                $this->loadMetadata($realClassName, $metadataBuildingContext);
219
            }
220
        } catch (MappingException $loadingException) {
221
            if (! $fallbackMetadataResponse = $this->onNotFoundMetadata($realClassName, $metadataBuildingContext)) {
222
                throw $loadingException;
223
            }
224
225
            $this->loadedMetadata[$realClassName] = $fallbackMetadataResponse;
226
        }
227
228
        if ($className !== $realClassName) {
229
            // We do not have the alias name in the map, include it
230
            $this->loadedMetadata[$className] = $this->loadedMetadata[$realClassName];
231
        }
232
233
        $metadataBuildingContext->validate();
234
235
        return $this->loadedMetadata[$className];
0 ignored issues
show
Bug Best Practice introduced by
The return type of return $this->loadedMetadata[$className]; (Doctrine\ORM\Mapping\ClassMetadata) is incompatible with the return type declared by the interface Doctrine\Common\Persiste...Factory::getMetadataFor of type Doctrine\Common\Persistence\Mapping\ClassMetadata.

If you return a value from a function or method, it should be a sub-type of the type that is given by the parent type f.e. an interface, or abstract method. This is more formally defined by the Lizkov substitution principle, and guarantees that classes that depend on the parent type can use any instance of a child type interchangably. This principle also belongs to the SOLID principles for object oriented design.

Let’s take a look at an example:

class Author {
    private $name;

    public function __construct($name) {
        $this->name = $name;
    }

    public function getName() {
        return $this->name;
    }
}

abstract class Post {
    public function getAuthor() {
        return 'Johannes';
    }
}

class BlogPost extends Post {
    public function getAuthor() {
        return new Author('Johannes');
    }
}

class ForumPost extends Post { /* ... */ }

function my_function(Post $post) {
    echo strtoupper($post->getAuthor());
}

Our function my_function expects a Post object, and outputs the author of the post. The base class Post returns a simple string and outputting a simple string will work just fine. However, the child class BlogPost which is a sub-type of Post instead decided to return an object, and is therefore violating the SOLID principles. If a BlogPost were passed to my_function, PHP would not complain, but ultimately fail when executing the strtoupper call in its body.

Loading history...
236
    }
237
238
    /**
239
     * Checks whether the factory has the metadata for a class loaded already.
240
     *
241
     * @param string $className
242
     *
243
     * @return boolean TRUE if the metadata of the class in question is already loaded, FALSE otherwise.
244
     */
245
    public function hasMetadataFor($className)
246
    {
247
        return isset($this->loadedMetadata[$className]);
248
    }
249
250
    /**
251
     * Sets the metadata descriptor for a specific class.
252
     *
253
     * NOTE: This is only useful in very special cases, like when generating proxy classes.
254
     *
255
     * @param string        $className
256
     * @param ClassMetadata $class
257
     *
258
     * @return void
259
     */
260
    public function setMetadataFor($className, $class)
261
    {
262
        $this->loadedMetadata[$className] = $class;
263
    }
264
265
    /**
266
     * Gets an array of parent classes for the given entity class.
267
     *
268
     * @param string $name
269
     *
270
     * @return array
271
     */
272
    protected function getParentClasses($name)
273
    {
274
        // Collect parent classes, ignoring transient (not-mapped) classes.
275
        $parentClasses = [];
276
277
        foreach (array_reverse($this->getReflectionService()->getParentClasses($name)) as $parentClass) {
278
            if ( ! $this->getDriver()->isTransient($parentClass)) {
279
                $parentClasses[] = $parentClass;
280
            }
281
        }
282
283
        return $parentClasses;
284
    }
285
286
    /**
287
     * Loads the metadata of the class in question and all it's ancestors whose metadata
288
     * is still not loaded.
289
     *
290
     * Important: The class $name does not necessarily exist at this point here.
291
     * Scenarios in a code-generation setup might have access to XML/YAML
292
     * Mapping files without the actual PHP code existing here. That is why the
293
     * {@see Doctrine\Common\Persistence\Mapping\ReflectionService} interface
294
     * should be used for reflection.
295
     *
296
     * @param string                       $name                    The name of the class for which the metadata should
297
     *                                                              get loaded.
298
     * @param ClassMetadataBuildingContext $metadataBuildingContext
299
     *
300
     * @return array
301
     */
302
    protected function loadMetadata(string $name, ClassMetadataBuildingContext $metadataBuildingContext)
303
    {
304
        if ( ! $this->initialized) {
305
            $this->initialize();
306
        }
307
308
        $loaded = [];
309
310
        $parentClasses = $this->getParentClasses($name);
311
        $parentClasses[] = $name;
312
313
        // Move down the hierarchy of parent classes, starting from the topmost class
314
        $parent = null;
315
        $rootEntityFound = false;
316
        $reflService = $this->getReflectionService();
317
318
        foreach ($parentClasses as $className) {
319
            if (isset($this->loadedMetadata[$className])) {
320
                $parent = $this->loadedMetadata[$className];
321
322
                if ($this->isEntity($parent)) {
323
                    $rootEntityFound = true;
324
                }
325
326
                continue;
327
            }
328
329
            $class = $this->newClassMetadataInstance($className, $metadataBuildingContext);
330
331
            if ($parent) {
332
                $class->setParent($parent);
333
            }
334
335
            $this->initializeReflection($class, $reflService);
336
            $this->doLoadMetadata($class, $metadataBuildingContext, $rootEntityFound);
337
338
            $this->loadedMetadata[$className] = $class;
339
340
            $parent = $class;
341
342
            if ($this->isEntity($class)) {
343
                $rootEntityFound = true;
344
            }
345
346
            $this->wakeupReflection($class, $reflService);
347
348
            $loaded[] = $className;
349
        }
350
351
        return $loaded;
352
    }
353
354
    /**
355
     * Provides a fallback hook for loading metadata when loading failed due to reflection/mapping exceptions
356
     *
357
     * Override this method to implement a fallback strategy for failed metadata loading
358
     *
359
     * @param string                       $className
360
     * @param ClassMetadataBuildingContext $metadataBuildingContext
361
     *
362
     * @return \Doctrine\ORM\Mapping\ClassMetadata|null
363
     */
364
    protected function onNotFoundMetadata($className, ClassMetadataBuildingContext $metadataBuildingContext)
365
    {
366
        return null;
367
    }
368
369
    /**
370
     * Actually loads the metadata from the underlying metadata.
371
     *
372
     * @param ClassMetadata                $class
373
     * @param ClassMetadataBuildingContext $metadataBuildingContext
374
     * @param bool                         $rootEntityFound
375
     *
376
     * @return void
377
     */
378
    abstract protected function doLoadMetadata(
379
        ClassMetadata $class,
380
        ClassMetadataBuildingContext $metadataBuildingContext,
381
        bool $rootEntityFound
382
    ) : void;
383
384
    /**
385
     * Creates a new ClassMetadata instance for the given class name.
386
     *
387
     * @param string                       $className
388
     * @param ClassMetadataBuildingContext $metadataBuildingContext
389
     *
390
     * @return ClassMetadata
391
     */
392
    abstract protected function newClassMetadataInstance(
393
        string $className,
394
        ClassMetadataBuildingContext $metadataBuildingContext
395
    ) : ClassMetadata;
396
397
    /**
398
     * Creates a new ClassMetadataBuildingContext instance.
399
     *
400
     * @return ClassMetadataBuildingContext
401
     */
402
    abstract protected function newClassMetadataBuildingContext() : ClassMetadataBuildingContext;
403
404
    /**
405
     * {@inheritDoc}
406
     */
407
    public function isTransient($class) : bool
408
    {
409
        if ( ! $this->initialized) {
410
            $this->initialize();
411
        }
412
413
        // Check for namespace alias
414
        if (strpos($class, ':') !== false) {
415
            list($namespaceAlias, $simpleClassName) = explode(':', $class, 2);
416
417
            $class = $this->getFqcnFromAlias($namespaceAlias, $simpleClassName);
418
        }
419
420
        return $this->getDriver()->isTransient($class);
421
    }
422
423
    /**
424
     * Sets the reflectionService.
425
     *
426
     * @param ReflectionService $reflectionService
427
     *
428
     * @return void
429
     */
430
    public function setReflectionService(ReflectionService $reflectionService)
431
    {
432
        $this->reflectionService = $reflectionService;
433
    }
434
435
    /**
436
     * Gets the reflection service associated with this metadata factory.
437
     *
438
     * @return ReflectionService
439
     */
440
    public function getReflectionService()
441
    {
442
        if ($this->reflectionService === null) {
443
            $this->reflectionService = new RuntimeReflectionService();
444
        }
445
446
        return $this->reflectionService;
447
    }
448
}
449