Failed Conditions
Pull Request — master (#6935)
by Michael
95:58
created

AbstractClassMetadataFactory   A

Complexity

Total Complexity 31

Size/Duplication

Total Lines 322
Duplicated Lines 0 %

Test Coverage

Coverage 89.16%

Importance

Changes 0
Metric Value
wmc 31
c 0
b 0
f 0
dl 0
loc 322
ccs 74
cts 83
cp 0.8916
rs 9.8

14 Methods

Rating   Name   Duplication   Size   Complexity  
A setReflectionService() 0 3 1
C getMetadataFor() 0 52 9
A getLoadedMetadata() 0 3 1
A onNotFoundMetadata() 0 5 1
A getReflectionService() 0 7 2
A getParentClasses() 0 12 3
A isTransient() 0 7 2
A setCacheDriver() 0 3 1
A hasMetadataFor() 0 3 1
A setMetadataFor() 0 3 1
A getAllMetadata() 0 14 3
A getCacheDriver() 0 3 1
B loadMetadata() 0 31 4
A normalizeClassName() 0 3 1
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\Persistence\Mapping\ClassMetadataFactory;
0 ignored issues
show
Bug introduced by
This use statement conflicts with another class in this namespace, Doctrine\ORM\Mapping\ClassMetadataFactory. Consider defining an alias.

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\Persistence\Mapping\MappingException as CommonMappingException;
10
use Doctrine\ORM\Reflection\ReflectionService;
11
use Doctrine\ORM\Reflection\RuntimeReflectionService;
12
use Doctrine\ORM\Utility\StaticClassNameConverter;
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
abstract class AbstractClassMetadataFactory implements ClassMetadataFactory
23
{
24
    /**
25
     * Salt used by specific Object Manager implementation.
26
     *
27
     * @var string
28
     */
29
    protected $cacheSalt = '$CLASSMETADATA';
30
31
    /**
32
     * @var \Doctrine\Common\Cache\Cache|null
33
     */
34
    private $cacheDriver;
35
36
    /**
37
     * @var ClassMetadata[]
38
     */
39
    private $loadedMetadata = [];
40
41
    /**
42
     * @var bool
43
     */
44
    protected $initialized = false;
45
46
    /**
47
     * @var ReflectionService|null
48
     */
49
    protected $reflectionService;
50
51
    /**
52
     * Sets the cache driver used by the factory to cache ClassMetadata instances.
53
     */
54 2259
    public function setCacheDriver(?Cache $cacheDriver = null) : void
55
    {
56 2259
        $this->cacheDriver = $cacheDriver;
57 2259
    }
58
59
    /**
60
     * Gets the cache driver used by the factory to cache ClassMetadata instances.
61
     */
62
    public function getCacheDriver() : ?Cache
63
    {
64
        return $this->cacheDriver;
65
    }
66
67
    /**
68
     * Returns an array of all the loaded metadata currently in memory.
69
     *
70
     * @return ClassMetadata[]
71
     */
72
    public function getLoadedMetadata() : array
73
    {
74
        return $this->loadedMetadata;
75
    }
76
77
    /**
78
     * Sets the reflectionService.
79
     */
80
    public function setReflectionService(ReflectionService $reflectionService) : void
81
    {
82
        $this->reflectionService = $reflectionService;
83
    }
84
85
    /**
86
     * Gets the reflection service associated with this metadata factory.
87
     */
88 1958
    public function getReflectionService() : ReflectionService
89
    {
90 1958
        if ($this->reflectionService === null) {
91 1958
            $this->reflectionService = new RuntimeReflectionService();
92
        }
93
94 1958
        return $this->reflectionService;
95
    }
96
97
    /**
98
     * Checks whether the factory has the metadata for a class loaded already.
99
     *
100
     * @param string $className
101
     *
102
     * @return bool TRUE if the metadata of the class in question is already loaded, FALSE otherwise.
103
     */
104 81
    public function hasMetadataFor($className) : bool
105
    {
106 81
        return isset($this->loadedMetadata[$className]);
107
    }
108
109
    /**
110
     * Sets the metadata descriptor for a specific class.
111
     *
112
     * NOTE: This is only useful in very special cases, like when generating proxy classes.
113
     *
114
     * @param string        $className
115
     * @param ClassMetadata $class
116
     */
117 5
    public function setMetadataFor($className, $class) : void
118
    {
119 5
        $this->loadedMetadata[$className] = $class;
120 5
    }
121
122
    /**
123
     * Forces the factory to load the metadata of all classes known to the underlying
124
     * mapping driver.
125
     *
126
     * @return ClassMetadata[] The ClassMetadata instances of all mapped classes.
127
     *
128
     * @throws \InvalidArgumentException
129
     * @throws \ReflectionException
130
     * @throws MappingException
131
     */
132 56
    public function getAllMetadata() : array
133
    {
134 56
        if (! $this->initialized) {
135 56
            $this->initialize();
136
        }
137
138 56
        $driver   = $this->getDriver();
139 56
        $metadata = [];
140
141 56
        foreach ($driver->getAllClassNames() as $className) {
142 55
            $metadata[] = $this->getMetadataFor($className);
143
        }
144
145 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...
146
    }
147
148
    /**
149
     * Gets the class metadata descriptor for a class.
150
     *
151
     * @param string $className The name of the class.
152
     *
153
     * @throws \InvalidArgumentException
154
     * @throws ReflectionException
155
     * @throws CommonMappingException
156
     */
157 1963
    public function getMetadataFor($className) : ClassMetadata
158
    {
159 1963
        if (isset($this->loadedMetadata[$className])) {
160 1636
            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...
161
        }
162
163 1958
        $realClassName = $this->normalizeClassName($className);
164
165 1958
        if (isset($this->loadedMetadata[$realClassName])) {
166
            // We do not have the alias name in the map, include it
167 215
            return $this->loadedMetadata[$className] = $this->loadedMetadata[$realClassName];
168
        }
169
170 1958
        $metadataBuildingContext = $this->newClassMetadataBuildingContext();
171 1958
        $loadingException        = null;
0 ignored issues
show
Unused Code introduced by
The assignment to $loadingException is dead and can be removed.
Loading history...
172
173
        try {
174 1958
            if ($this->cacheDriver) {
175 1851
                $cached = $this->cacheDriver->fetch($realClassName . $this->cacheSalt);
176
177 1851
                if ($cached instanceof ClassMetadata) {
178 1630
                    $this->loadedMetadata[$realClassName] = $cached;
179
180 1630
                    $cached->wakeupReflection($metadataBuildingContext->getReflectionService());
181
                } else {
182 270
                    foreach ($this->loadMetadata($realClassName, $metadataBuildingContext) as $loadedClass) {
183 260
                        $loadedClassName = $loadedClass->getClassName();
184
185 1843
                        $this->cacheDriver->save($loadedClassName . $this->cacheSalt, $loadedClass, null);
186
                    }
187
                }
188
            } else {
189 1950
                $this->loadMetadata($realClassName, $metadataBuildingContext);
190
            }
191 22
        } catch (CommonMappingException $loadingException) {
192 12
            $fallbackMetadataResponse = $this->onNotFoundMetadata($realClassName, $metadataBuildingContext);
193
194 12
            if (! $fallbackMetadataResponse) {
195 10
                throw $loadingException;
196
            }
197
198 2
            $this->loadedMetadata[$realClassName] = $fallbackMetadataResponse;
199
        }
200
201 1941
        if ($className !== $realClassName) {
202
            // We do not have the alias name in the map, include it
203 3
            $this->loadedMetadata[$className] = $this->loadedMetadata[$realClassName];
204
        }
205
206 1941
        $metadataBuildingContext->validate();
207
208 1941
        return $this->loadedMetadata[$className];
209
    }
210
211
    /**
212
     * Loads the metadata of the class in question and all it's ancestors whose metadata
213
     * is still not loaded.
214
     *
215
     * Important: The class $name does not necessarily exist at this point here.
216
     * Scenarios in a code-generation setup might have access to XML/YAML
217
     * Mapping files without the actual PHP code existing here. That is why the
218
     * {@see Doctrine\Common\Persistence\Mapping\ReflectionService} interface
219
     * should be used for reflection.
220
     *
221
     * @param string $name The name of the class for which the metadata should
222
     *                                                              get loaded.
223
     *
224
     * @return ClassMetadata[]
225
     *
226
     * @throws \InvalidArgumentException
227
     */
228 378
    protected function loadMetadata(string $name, ClassMetadataBuildingContext $metadataBuildingContext) : array
229
    {
230 378
        if (! $this->initialized) {
231 373
            $this->initialize();
232
        }
233
234 378
        $loaded = [];
235
236 378
        $parentClasses   = $this->getParentClasses($name);
237 367
        $parentClasses[] = $name;
238
239
        // Move down the hierarchy of parent classes, starting from the topmost class
240 367
        $parent = null;
241
242 367
        foreach ($parentClasses as $className) {
243 367
            if (isset($this->loadedMetadata[$className])) {
244 73
                $parent = $this->loadedMetadata[$className];
245
246 73
                continue;
247
            }
248
249 367
            $class = $this->doLoadMetadata($className, $parent, $metadataBuildingContext);
250
251 361
            $this->loadedMetadata[$className] = $class;
252
253 361
            $parent = $class;
254
255 361
            $loaded[] = $class;
256
        }
257
258 358
        return $loaded;
259
    }
260
261
    /**
262
     * {@inheritDoc}
263
     */
264 47
    public function isTransient($className) : bool
265
    {
266 47
        if (! $this->initialized) {
267 15
            $this->initialize();
268
        }
269
270 47
        return $this->getDriver()->isTransient($this->normalizeClassName($className));
271
    }
272
273
    /**
274
     * Gets an array of parent classes for the given entity class.
275
     *
276
     * @param string $name
277
     *
278
     * @return string[]
279
     *
280
     * @throws \InvalidArgumentException
281
     */
282 378
    protected function getParentClasses($name) : array
283
    {
284
        // Collect parent classes, ignoring transient (not-mapped) classes.
285 378
        $parentClasses = [];
286
287 378
        foreach (array_reverse($this->getReflectionService()->getParentClasses($name)) as $parentClass) {
288 105
            if (! $this->getDriver()->isTransient($parentClass)) {
289 105
                $parentClasses[] = $parentClass;
290
            }
291
        }
292
293 367
        return $parentClasses;
294
    }
295
296
    /**
297
     * Provides a fallback hook for loading metadata when loading failed due to reflection/mapping exceptions
298
     *
299
     * Override this method to implement a fallback strategy for failed metadata loading
300
     */
301
    protected function onNotFoundMetadata(
302
        string $className,
303
        ClassMetadataBuildingContext $metadataBuildingContext
304
    ) : ?ClassMetadata {
305
        return null;
306
    }
307
308 1959
    private function normalizeClassName(string $className) : string
309
    {
310 1959
        return StaticClassNameConverter::getRealClass($className);
311
    }
312
313
    /**
314
     * Lazy initialization of this stuff, especially the metadata driver,
315
     * since these are not needed at all when a metadata cache is active.
316
     */
317
    abstract protected function initialize() : void;
318
319
    /**
320
     * Returns the mapping driver implementation.
321
     */
322
    abstract protected function getDriver() : Driver\MappingDriver;
323
324
    /**
325
     * Checks whether the class metadata is an entity.
326
     *
327
     * This method should return false for mapped superclasses or embedded classes.
328
     */
329
    abstract protected function isEntity(ClassMetadata $class) : bool;
330
331
    /**
332
     * Creates a new ClassMetadata instance for the given class name.
333
     */
334
    abstract protected function doLoadMetadata(
335
        string $className,
336
        ?ClassMetadata $parent,
337
        ClassMetadataBuildingContext $metadataBuildingContext
338
    ) : ClassMetadata;
339
340
    /**
341
     * Creates a new ClassMetadataBuildingContext instance.
342
     */
343
    abstract protected function newClassMetadataBuildingContext() : ClassMetadataBuildingContext;
344
}
345