Passed
Pull Request — master (#70)
by Alexander M.
06:58
created

AbstractClassMetadataFactory::setCache()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 3
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 2
CRAP Score 1

Importance

Changes 0
Metric Value
eloc 1
dl 0
loc 3
ccs 2
cts 2
cp 1
rs 10
c 0
b 0
f 0
cc 1
nc 1
nop 1
crap 1
1
<?php
2
3
declare(strict_types=1);
4
5
namespace Doctrine\Persistence\Mapping;
6
7
use Doctrine\Common\Cache\Cache;
8
use Doctrine\Persistence\Mapping\Driver\MappingDriver;
9
use Doctrine\Persistence\Proxy;
10
use Doctrine\Persistence\SimpleCacheAdapter;
11
use Psr\SimpleCache\CacheInterface;
12
use ReflectionException;
13
use function array_reverse;
14
use function array_unshift;
15
use function explode;
16
use function strpos;
17
use function strrpos;
18
use function substr;
19
20
/**
21
 * The ClassMetadataFactory is used to create ClassMetadata objects that contain all the
22
 * metadata mapping informations of a class which describes how a class should be mapped
23
 * to a relational database.
24
 *
25
 * This class was abstracted from the ORM ClassMetadataFactory.
26
 */
27
abstract class AbstractClassMetadataFactory implements ClassMetadataFactory
28
{
29
    /**
30
     * Salt used by specific Object Manager implementation.
31
     *
32
     * @var string
33
     */
34
    protected $cacheSalt = '$CLASSMETADATA';
35
36
    /** @var CacheInterface|null */
37
    private $cache;
38
39
    /** @var array<string, ClassMetadata> */
40
    private $loadedMetadata = [];
41
42
    /** @var bool */
43
    protected $initialized = false;
44
45
    /** @var ReflectionService|null */
46
    private $reflectionService = null;
47
48
    /**
49
     * Sets the cache driver used by the factory to cache ClassMetadata instances.
50
     *
51
     * @deprecated
52
     */
53 4
    public function setCacheDriver(?Cache $cacheDriver = null) : void
54
    {
55 4
        @trigger_error(sprintf('%s is deprecated. Use setCache() with a PSR-16 cache instead.', __METHOD__), E_USER_DEPRECATED);
0 ignored issues
show
introduced by
Function trigger_error() should not be referenced via a fallback global name, but via a use statement.
Loading history...
introduced by
Function sprintf() should not be referenced via a fallback global name, but via a use statement.
Loading history...
introduced by
Constant E_USER_DEPRECATED should not be referenced via a fallback global name, but via a use statement.
Loading history...
56
57 4
        $this->cache = new SimpleCacheAdapter($cacheDriver);
0 ignored issues
show
Bug introduced by
It seems like $cacheDriver can also be of type null; however, parameter $wrapped of Doctrine\Persistence\Sim...eAdapter::__construct() does only seem to accept Doctrine\Common\Cache\Cache, 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

57
        $this->cache = new SimpleCacheAdapter(/** @scrutinizer ignore-type */ $cacheDriver);
Loading history...
58 4
    }
59
60
    /**
61
     * Gets the cache driver used by the factory to cache ClassMetadata instances.
62
     *
63
     * @deprecated
64
     */
65 1
    public function getCacheDriver() : ?Cache
66
    {
67 1
        @trigger_error(sprintf('%s is deprecated.', __METHOD__), E_USER_DEPRECATED);
0 ignored issues
show
introduced by
Function trigger_error() should not be referenced via a fallback global name, but via a use statement.
Loading history...
introduced by
Function sprintf() should not be referenced via a fallback global name, but via a use statement.
Loading history...
introduced by
Constant E_USER_DEPRECATED should not be referenced via a fallback global name, but via a use statement.
Loading history...
68
69 1
        if ($this->cache !== null && !$this->cache instanceof SimpleCacheAdapter) {
0 ignored issues
show
Coding Style introduced by
Expected 1 space(s) after NOT operator; 0 found
Loading history...
70
            throw new \BadMethodCallException('Cannot convert a PSR-16 cache back to a Doctrine cache.');
0 ignored issues
show
introduced by
Class \BadMethodCallException should not be referenced via a fully qualified name, but via a use statement.
Loading history...
71
        }
72
73 1
        return $this->cache === null ? null : $this->cache->unwrap();
74
    }
75
76 2
    public function setCache(?CacheInterface $cache) : void
77
    {
78 2
        $this->cache = $cache;
79 2
    }
80
81
    /**
82
     * Returns an array of all the loaded metadata currently in memory.
83
     *
84
     * @return ClassMetadata[]
85
     */
86
    public function getLoadedMetadata() : array
87
    {
88
        return $this->loadedMetadata;
89
    }
90
91
    /**
92
     * Forces the factory to load the metadata of all classes known to the underlying
93
     * mapping driver.
94
     *
95
     * @return array<int, ClassMetadata> The ClassMetadata instances of all mapped classes.
96
     */
97
    public function getAllMetadata() : array
98
    {
99
        if (! $this->initialized) {
100
            $this->initialize();
101
        }
102
103
        $driver   = $this->getDriver();
104
        $metadata = [];
105
        foreach ($driver->getAllClassNames() as $className) {
106
            $metadata[] = $this->getMetadataFor($className);
107
        }
108
109
        return $metadata;
110
    }
111
112
    /**
113
     * Lazy initialization of this stuff, especially the metadata driver,
114
     * since these are not needed at all when a metadata cache is active.
115
     */
116
    abstract protected function initialize() : void;
117
118
    /**
119
     * Gets the fully qualified class-name from the namespace alias.
120
     */
121
    abstract protected function getFqcnFromAlias(
122
        string $namespaceAlias,
123
        string $simpleClassName
124
    ) : string;
125
126
    /**
127
     * Returns the mapping driver implementation.
128
     */
129
    abstract protected function getDriver() : MappingDriver;
130
131
    /**
132
     * Wakes up reflection after ClassMetadata gets unserialized from cache.
133
     */
134
    abstract protected function wakeupReflection(
135
        ClassMetadata $class,
136
        ReflectionService $reflService
137
    ) : void;
138
139
    /**
140
     * Initializes Reflection after ClassMetadata was constructed.
141
     */
142
    abstract protected function initializeReflection(
143
        ClassMetadata $class,
144
        ReflectionService $reflService
145
    ) : void;
146
147
    /**
148
     * Checks whether the class metadata is an entity.
149
     *
150
     * This method should return false for mapped superclasses or embedded classes.
151
     */
152
    abstract protected function isEntity(ClassMetadata $class) : bool;
153
154
    /**
155
     * Gets the class metadata descriptor for a class.
156
     *
157
     * @param string $className The name of the class.
158
     *
159
     * @throws ReflectionException
160
     * @throws MappingException
161
     */
162 12
    public function getMetadataFor(string $className) : ClassMetadata
163
    {
164 12
        if (isset($this->loadedMetadata[$className])) {
165
            return $this->loadedMetadata[$className];
166
        }
167
168
        // Check for namespace alias
169 12
        if (strpos($className, ':') !== false) {
170 2
            [$namespaceAlias, $simpleClassName] = explode(':', $className, 2);
171
172 2
            $realClassName = $this->getFqcnFromAlias($namespaceAlias, $simpleClassName);
173
        } else {
174 10
            $realClassName = $this->getRealClass($className);
175
        }
176
177 12
        if (isset($this->loadedMetadata[$realClassName])) {
178
            // We do not have the alias name in the map, include it
179
            return $this->loadedMetadata[$className] = $this->loadedMetadata[$realClassName];
180
        }
181
182 12
        $loadingException = null;
0 ignored issues
show
Unused Code introduced by
The assignment to $loadingException is dead and can be removed.
Loading history...
183
184
        try {
185 12
            if ($this->cache !== null) {
186 5
                $cacheKey = str_replace('\\', '.', $realClassName) . $this->cacheSalt;
0 ignored issues
show
introduced by
Function str_replace() should not be referenced via a fallback global name, but via a use statement.
Loading history...
187 5
                $cached = $this->cache->get($cacheKey);
0 ignored issues
show
Coding Style introduced by
Equals sign not aligned with surrounding assignments; expected 3 spaces but found 1 space

This check looks for multiple assignments in successive lines of code. It will report an issue if the operators are not in a straight line.

To visualize

$a = "a";
$ab = "ab";
$abc = "abc";

will produce issues in the first and second line, while this second example

$a   = "a";
$ab  = "ab";
$abc = "abc";

will produce no issues.

Loading history...
188
189 5
                if ($cached instanceof ClassMetadata) {
190 2
                    $this->loadedMetadata[$realClassName] = $cached;
191
192 2
                    $this->wakeupReflection($cached, $this->getReflectionService());
193
                } else {
194 5
                    foreach ($this->loadMetadata($realClassName) as $loadedClassName) {
195 2
                        $this->cache->set(
196 2
                            $cacheKey,
197 2
                            $this->loadedMetadata[$loadedClassName]
198
                        );
199
                    }
200
                }
201
            } else {
202 11
                $this->loadMetadata($realClassName);
203
            }
204 5
        } catch (MappingException $loadingException) {
205 5
            $fallbackMetadataResponse = $this->onNotFoundMetadata($realClassName);
0 ignored issues
show
Bug introduced by
Are you sure the assignment to $fallbackMetadataResponse is correct as $this->onNotFoundMetadata($realClassName) targeting Doctrine\Persistence\Map...y::onNotFoundMetadata() seems to always return null.

This check looks for function or method calls that always return null and whose return value is assigned to a variable.

class A
{
    function getObject()
    {
        return null;
    }

}

$a = new A();
$object = $a->getObject();

The method getObject() can return nothing but null, so it makes no sense to assign that value to a variable.

The reason is most likely that a function or method is imcomplete or has been reduced for debug purposes.

Loading history...
206
207 5
            if ($fallbackMetadataResponse === null) {
0 ignored issues
show
introduced by
The condition $fallbackMetadataResponse === null is always true.
Loading history...
208 3
                throw $loadingException;
209
            }
210
211 2
            $this->loadedMetadata[$realClassName] = $fallbackMetadataResponse;
212
        }
213
214 9
        if ($className !== $realClassName) {
215
            // We do not have the alias name in the map, include it
216 1
            $this->loadedMetadata[$className] = $this->loadedMetadata[$realClassName];
217
        }
218
219 9
        return $this->loadedMetadata[$className];
220
    }
221
222
    /**
223
     * Checks whether the factory has the metadata for a class loaded already.
224
     *
225
     * @return bool TRUE if the metadata of the class in question is already loaded, FALSE otherwise.
226
     */
227 3
    public function hasMetadataFor(string $className) : bool
228
    {
229 3
        return isset($this->loadedMetadata[$className]);
230
    }
231
232
    /**
233
     * Sets the metadata descriptor for a specific class.
234
     *
235
     * NOTE: This is only useful in very special cases, like when generating proxy classes.
236
     */
237
    public function setMetadataFor(string $className, ClassMetadata $class) : void
238
    {
239
        $this->loadedMetadata[$className] = $class;
240
    }
241
242
    /**
243
     * Gets an array of parent classes for the given entity class.
244
     *
245
     * @return array<int, string>
246
     */
247 10
    protected function getParentClasses(string $name) : array
248
    {
249
        // Collect parent classes, ignoring transient (not-mapped) classes.
250 10
        $parentClasses = [];
0 ignored issues
show
Unused Code introduced by
The assignment to $parentClasses is dead and can be removed.
Loading history...
251
252 10
        $parentClasses = $this->getReflectionService()
253 10
            ->getParentClasses($name);
254
255 5
        foreach (array_reverse($parentClasses) as $parentClass) {
256 4
            if ($this->getDriver()->isTransient($parentClass)) {
257
                continue;
258
            }
259
260 4
            $parentClasses[] = $parentClass;
261
        }
262
263 5
        return $parentClasses;
264
    }
265
266
    /**
267
     * Loads the metadata of the class in question and all it's ancestors whose metadata
268
     * is still not loaded.
269
     *
270
     * Important: The class $name does not necessarily exist at this point here.
271
     * Scenarios in a code-generation setup might have access to XML/YAML
272
     * Mapping files without the actual PHP code existing here. That is why the
273
     * {@see Doctrine\Persistence\Mapping\ReflectionService} interface
274
     * should be used for reflection.
275
     *
276
     * @param string $name The name of the class for which the metadata should get loaded.
277
     *
278
     * @return array<int, string>
279
     */
280 10
    protected function loadMetadata(string $name) : array
281
    {
282 10
        if (! $this->initialized) {
283 10
            $this->initialize();
284
        }
285
286 10
        $loaded = [];
287
288 10
        $parentClasses   = $this->getParentClasses($name);
289 5
        $parentClasses[] = $name;
290
291
        // Move down the hierarchy of parent classes, starting from the topmost class
292 5
        $parent          = null;
293 5
        $rootEntityFound = false;
294 5
        $visited         = [];
295 5
        $reflService     = $this->getReflectionService();
296
297 5
        foreach ($parentClasses as $className) {
298 5
            if (isset($this->loadedMetadata[$className])) {
299 4
                $parent = $this->loadedMetadata[$className];
300
301 4
                if ($this->isEntity($parent)) {
302 4
                    $rootEntityFound = true;
303
304 4
                    array_unshift($visited, $className);
305
                }
306
307 4
                continue;
308
            }
309
310 5
            $class = $this->newClassMetadataInstance($className);
311 5
            $this->initializeReflection($class, $reflService);
312
313 5
            $this->doLoadMetadata($class, $parent, $rootEntityFound, $visited);
314
315 5
            $this->loadedMetadata[$className] = $class;
316
317 5
            $parent = $class;
318
319 5
            if ($this->isEntity($class)) {
320 5
                $rootEntityFound = true;
321
322 5
                array_unshift($visited, $className);
323
            }
324
325 5
            $this->wakeupReflection($class, $reflService);
326
327 5
            $loaded[] = $className;
328
        }
329
330 5
        return $loaded;
331
    }
332
333
    /**
334
     * Provides a fallback hook for loading metadata when loading failed due to reflection/mapping exceptions
335
     *
336
     * Override this method to implement a fallback strategy for failed metadata loading
337
     */
338
    protected function onNotFoundMetadata(string $className) : ?ClassMetadata
339
    {
340
        return null;
341
    }
342
343
    /**
344
     * Actually loads the metadata from the underlying metadata.
345
     *
346
     * @param array<int, string> $nonSuperclassParents All parent class names that are not marked as mapped superclasses.
347
     */
348
    abstract protected function doLoadMetadata(
349
        ClassMetadata $class,
350
        ?ClassMetadata $parent,
351
        bool $rootEntityFound,
352
        array $nonSuperclassParents
353
    ) : void;
354
355
    /**
356
     * Creates a new ClassMetadata instance for the given class name.
357
     */
358
    abstract protected function newClassMetadataInstance(string $className) : ClassMetadata;
359
360
    /**
361
     * {@inheritDoc}
362
     */
363
    public function isTransient(string $class) : bool
364
    {
365
        if (! $this->initialized) {
366
            $this->initialize();
367
        }
368
369
        // Check for namespace alias
370
        if (strpos($class, ':') !== false) {
371
            [$namespaceAlias, $simpleClassName] = explode(':', $class, 2);
372
373
            $class = $this->getFqcnFromAlias($namespaceAlias, $simpleClassName);
374
        }
375
376
        return $this->getDriver()->isTransient($class);
377
    }
378
379
    /**
380
     * Sets the reflectionService.
381
     */
382
    public function setReflectionService(ReflectionService $reflectionService) : void
383
    {
384
        $this->reflectionService = $reflectionService;
385
    }
386
387
    /**
388
     * Gets the reflection service associated with this metadata factory.
389
     */
390 12
    public function getReflectionService() : ReflectionService
391
    {
392 12
        if ($this->reflectionService === null) {
393 12
            $this->reflectionService = new RuntimeReflectionService();
394
        }
395
396 12
        return $this->reflectionService;
397
    }
398
399
    /**
400
     * Gets the real class name of a class name that could be a proxy.
401
     */
402 10
    private function getRealClass(string $class) : string
403
    {
404 10
        $pos = strrpos($class, '\\' . Proxy::MARKER . '\\');
405
406 10
        if ($pos === false) {
407 10
            return $class;
408
        }
409
410
        return substr($class, $pos + Proxy::MARKER_LENGTH + 2);
411
    }
412
}
413