Completed
Push — master ( a0071b...e33605 )
by Michael
12s
created

lib/Doctrine/ORM/Cache/DefaultQueryCache.php (8 issues)

1
<?php
2
3
declare(strict_types=1);
4
5
namespace Doctrine\ORM\Cache;
6
7
use Doctrine\ORM\Cache;
8
use Doctrine\ORM\Cache\Persister\CachedPersister;
9
use Doctrine\ORM\EntityManagerInterface;
10
use Doctrine\ORM\Mapping\AssociationMetadata;
11
use Doctrine\ORM\Mapping\ToOneAssociationMetadata;
12
use Doctrine\ORM\Query;
13
use Doctrine\ORM\Query\ResultSetMapping;
14
use ProxyManager\Proxy\GhostObjectInterface;
15
16
/**
17
 * Default query cache implementation.
18
 */
19
class DefaultQueryCache implements QueryCache
20
{
21
    /**
22
     * @var \Doctrine\ORM\EntityManagerInterface
23
     */
24
    private $em;
25
26
    /**
27
     * @var \Doctrine\ORM\Cache\Region
28
     */
29
    private $region;
30
31
    /**
32
     * @var \Doctrine\ORM\Cache\QueryCacheValidator
33
     */
34
    private $validator;
35
36
    /**
37
     * @var \Doctrine\ORM\Cache\Logging\CacheLogger
38
     */
39
    protected $cacheLogger;
40
41
    /**
42
     * @var mixed[]
43
     */
44
    private static $hints = [Query::HINT_CACHE_ENABLED => true];
45
46
    /**
47
     * @param \Doctrine\ORM\EntityManagerInterface $em     The entity manager.
48
     * @param \Doctrine\ORM\Cache\Region           $region The query region.
49
     */
50 86
    public function __construct(EntityManagerInterface $em, Region $region)
51
    {
52 86
        $cacheConfig = $em->getConfiguration()->getSecondLevelCacheConfiguration();
53
54 86
        $this->em          = $em;
55 86
        $this->region      = $region;
56 86
        $this->cacheLogger = $cacheConfig->getCacheLogger();
57 86
        $this->validator   = $cacheConfig->getQueryValidator();
58 86
    }
59
60
    /**
61
     * {@inheritdoc}
62
     */
63 52
    public function get(QueryCacheKey $key, ResultSetMapping $rsm, array $hints = [])
64
    {
65 52
        if (! ($key->cacheMode & Cache::MODE_GET)) {
66 3
            return null;
67
        }
68
69 51
        $cacheEntry = $this->region->get($key);
70
71 51
        if (! $cacheEntry instanceof QueryCacheEntry) {
72 47
            return null;
73
        }
74
75 38
        if (! $this->validator->isValid($key, $cacheEntry)) {
76 8
            $this->region->evict($key);
77
78 8
            return null;
79
        }
80
81 33
        $result      = [];
82 33
        $entityName  = reset($rsm->aliasMap);
83 33
        $hasRelation = ( ! empty($rsm->relationMap));
84 33
        $unitOfWork  = $this->em->getUnitOfWork();
85 33
        $persister   = $unitOfWork->getEntityPersister($entityName);
86 33
        $region      = $persister->getCacheRegion();
0 ignored issues
show
The method getCacheRegion() does not exist on Doctrine\ORM\Persisters\Entity\EntityPersister. It seems like you code against a sub-type of Doctrine\ORM\Persisters\Entity\EntityPersister such as Doctrine\ORM\Cache\Persi...y\CachedEntityPersister. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

86
        /** @scrutinizer ignore-call */ 
87
        $region      = $persister->getCacheRegion();
Loading history...
87 33
        $regionName  = $region->getName();
88
89 33
        $cm = $this->em->getClassMetadata($entityName);
90
91 33
        $generateKeys = function (array $entry) use ($cm) : EntityCacheKey {
92 33
            return new EntityCacheKey($cm->getRootClassName(), $entry['identifier']);
0 ignored issues
show
The method getRootClassName() does not exist on Doctrine\Common\Persistence\Mapping\ClassMetadata. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

92
            return new EntityCacheKey($cm->/** @scrutinizer ignore-call */ getRootClassName(), $entry['identifier']);

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...
93 33
        };
94
95 33
        $cacheKeys = new CollectionCacheEntry(array_map($generateKeys, $cacheEntry->result));
96 33
        $entries   = $region->getMultiple($cacheKeys);
97
98
        // @TODO - move to cache hydration component
99 33
        foreach ($cacheEntry->result as $index => $entry) {
100 33
            $entityEntry = is_array($entries) ? ($entries[$index] ?? null) : null;
101
102 33
            if ($entityEntry === null) {
103 2
                if ($this->cacheLogger !== null) {
104 1
                    $this->cacheLogger->entityCacheMiss($regionName, $cacheKeys->identifiers[$index]);
105
                }
106
107 2
                return null;
108
            }
109
110 31
            if ($this->cacheLogger !== null) {
111 29
                $this->cacheLogger->entityCacheHit($regionName, $cacheKeys->identifiers[$index]);
112
            }
113
114 31
            if (! $hasRelation) {
115 22
                $result[$index] = $unitOfWork->createEntity(
116 22
                    $entityEntry->class,
117 22
                    $entityEntry->resolveAssociationEntries($this->em),
118 22
                    self::$hints
119
                );
120
121 22
                continue;
122
            }
123
124 9
            $data = $entityEntry->data;
125
126 9
            foreach ($entry['associations'] as $name => $assoc) {
127 9
                $assocPersister = $unitOfWork->getEntityPersister($assoc['targetEntity']);
128 9
                $assocRegion    = $assocPersister->getCacheRegion();
129 9
                $assocMetadata  = $this->em->getClassMetadata($assoc['targetEntity']);
130
131
                // *-to-one association
132 9
                if (isset($assoc['identifier'])) {
133 5
                    $assocKey = new EntityCacheKey($assocMetadata->getRootClassName(), $assoc['identifier']);
134
135 5
                    $assocEntry = $assocRegion->get($assocKey);
136 5
                    if ($assocEntry === null) {
137 1
                        if ($this->cacheLogger !== null) {
138 1
                            $this->cacheLogger->entityCacheMiss($assocRegion->getName(), $assocKey);
139
                        }
140
141 1
                        $unitOfWork->hydrationComplete();
142
143 1
                        return null;
144
                    }
145
146 4
                    $data[$name] = $unitOfWork->createEntity(
147 4
                        $assocEntry->class,
148 4
                        $assocEntry->resolveAssociationEntries($this->em),
149 4
                        self::$hints
150
                    );
151
152 4
                    if ($this->cacheLogger !== null) {
153 4
                        $this->cacheLogger->entityCacheHit($assocRegion->getName(), $assocKey);
154
                    }
155
156 4
                    continue;
157
                }
158
159 4
                if (! isset($assoc['list']) || empty($assoc['list'])) {
160
                    continue;
161
                }
162
163 4
                $generateKeys = function ($id) use ($assocMetadata) : EntityCacheKey {
164 4
                    return new EntityCacheKey($assocMetadata->getRootClassName(), $id);
165 4
                };
166
167 4
                $assocKeys    = new CollectionCacheEntry(array_map($generateKeys, $assoc['list']));
168 4
                $assocEntries = $assocRegion->getMultiple($assocKeys);
169
170
                // *-to-many association
171 4
                $collection = [];
172
173 4
                foreach ($assoc['list'] as $assocIndex => $assocId) {
174 4
                    $assocEntry = is_array($assocEntries) ? ($assocEntries[$assocIndex] ?? null) : null;
175
176 4
                    if ($assocEntry === null) {
177 1
                        if ($this->cacheLogger !== null) {
178 1
                            $this->cacheLogger->entityCacheMiss($assocRegion->getName(), $assocKeys->identifiers[$assocIndex]);
179
                        }
180
181 1
                        $unitOfWork->hydrationComplete();
182
183 1
                        return null;
184
                    }
185
186 3
                    $collection[$assocIndex] = $unitOfWork->createEntity(
187 3
                        $assocEntry->class,
188 3
                        $assocEntry->resolveAssociationEntries($this->em),
189 3
                        self::$hints
190
                    );
191
192 3
                    if ($this->cacheLogger !== null) {
193 3
                        $this->cacheLogger->entityCacheHit($assocRegion->getName(), $assocKeys->identifiers[$assocIndex]);
194
                    }
195
                }
196
197 3
                $data[$name] = $collection;
198
            }
199
200 7
            foreach ($data as $fieldName => $unCachedAssociationData) {
201
                // In some scenarios, such as EAGER+ASSOCIATION+ID+CACHE, the
202
                // cache key information in `$cacheEntry` will not contain details
203
                // for fields that are associations.
204
                //
205
                // This means that `$data` keys for some associations that may
206
                // actually not be cached will not be converted to actual association
207
                // data, yet they contain L2 cache AssociationCacheEntry objects.
208
                //
209
                // We need to unwrap those associations into proxy references,
210
                // since we don't have actual data for them except for identifiers.
211 7
                if ($unCachedAssociationData instanceof AssociationCacheEntry) {
212 4
                    $data[$fieldName] = $this->em->getReference(
213 4
                        $unCachedAssociationData->class,
214 7
                        $unCachedAssociationData->identifier
215
                    );
216
                }
217
            }
218
219 7
            $result[$index] = $unitOfWork->createEntity($entityEntry->class, $data, self::$hints);
220
        }
221
222 29
        $unitOfWork->hydrationComplete();
223
224 29
        return $result;
225
    }
226
227
    /**
228
     * {@inheritdoc}
229
     * @param mixed[] $hints
230
     */
231 58
    public function put(QueryCacheKey $key, ResultSetMapping $rsm, $result, array $hints = [])
232
    {
233 58
        if ($rsm->scalarMappings) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $rsm->scalarMappings 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...
234 1
            throw new CacheException('Second level cache does not support scalar results.');
235
        }
236
237 57
        if (count($rsm->entityMappings) > 1) {
238 1
            throw new CacheException('Second level cache does not support multiple root entities.');
239
        }
240
241 56
        if (! $rsm->isSelect) {
242 2
            throw new CacheException('Second-level cache query supports only select statements.');
243
        }
244
245 54
        if (isset($hints[Query::HINT_FORCE_PARTIAL_LOAD]) && $hints[Query::HINT_FORCE_PARTIAL_LOAD]) {
246 1
            throw new CacheException('Second level cache does not support partial entities.');
247
        }
248
249 53
        if (! ($key->cacheMode & Cache::MODE_PUT)) {
250 3
            return false;
251
        }
252
253 52
        $data       = [];
254 52
        $entityName = reset($rsm->aliasMap);
255 52
        $rootAlias  = key($rsm->aliasMap);
256 52
        $unitOfWork = $this->em->getUnitOfWork();
257 52
        $persister  = $unitOfWork->getEntityPersister($entityName);
258
259 52
        if (! ($persister instanceof CachedPersister)) {
260 1
            throw CacheException::nonCacheableEntity($entityName);
261
        }
262
263 51
        $region = $persister->getCacheRegion();
264
265 51
        foreach ($result as $index => $entity) {
266 51
            $identifier = $unitOfWork->getEntityIdentifier($entity);
267 51
            $entityKey  = new EntityCacheKey($entityName, $identifier);
268
269 51
            if (($key->cacheMode & Cache::MODE_REFRESH) || ! $region->contains($entityKey)) {
270
                // Cancel put result if entity put fail
271 36
                if (! $persister->storeEntityCache($entity, $entityKey)) {
0 ignored issues
show
The method storeEntityCache() does not exist on Doctrine\ORM\Persisters\Entity\EntityPersister. It seems like you code against a sub-type of Doctrine\ORM\Persisters\Entity\EntityPersister such as Doctrine\ORM\Cache\Persi...y\CachedEntityPersister. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

271
                if (! $persister->/** @scrutinizer ignore-call */ storeEntityCache($entity, $entityKey)) {
Loading history...
The method storeEntityCache() does not exist on Doctrine\ORM\Cache\Persister\CachedPersister. It seems like you code against a sub-type of Doctrine\ORM\Cache\Persister\CachedPersister such as Doctrine\ORM\Cache\Persi...y\CachedEntityPersister. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

271
                if (! $persister->/** @scrutinizer ignore-call */ storeEntityCache($entity, $entityKey)) {
Loading history...
272 1
                    return false;
273
                }
274
            }
275
276 50
            $data[$index]['identifier']   = $identifier;
277 50
            $data[$index]['associations'] = [];
278
279
            // @TODO - move to cache hydration components
280 50
            foreach ($rsm->relationMap as $alias => $name) {
281 15
                $parentAlias = $rsm->parentAliasMap[$alias];
282 15
                $parentClass = $rsm->aliasMap[$parentAlias];
283 15
                $metadata    = $this->em->getClassMetadata($parentClass);
284 15
                $association = $metadata->getProperty($name);
0 ignored issues
show
The method getProperty() does not exist on Doctrine\Common\Persistence\Mapping\ClassMetadata. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

284
                /** @scrutinizer ignore-call */ 
285
                $association = $metadata->getProperty($name);

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...
285 15
                $assocValue  = $this->getAssociationValue($rsm, $alias, $entity);
286
287 15
                if ($assocValue === null) {
288 1
                    continue;
289
                }
290
291
                // root entity association
292 14
                if ($rootAlias === $parentAlias) {
293
                    // Cancel put result if association put fail
294 14
                    $assocInfo = $this->storeAssociationCache($key, $association, $assocValue);
0 ignored issues
show
It seems like $assocValue can also be of type object; however, parameter $assocValue of Doctrine\ORM\Cache\Defau...storeAssociationCache() does only seem to accept array<mixed,mixed>, 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

294
                    $assocInfo = $this->storeAssociationCache($key, $association, /** @scrutinizer ignore-type */ $assocValue);
Loading history...
295 14
                    if ($assocInfo === null) {
296 2
                        return false;
297
                    }
298
299 12
                    $data[$index]['associations'][$name] = $assocInfo;
300
301 12
                    continue;
302
                }
303
304
                // store single nested association
305 2
                if (! is_array($assocValue)) {
306
                    // Cancel put result if association put fail
307 1
                    if ($this->storeAssociationCache($key, $association, $assocValue) === null) {
308
                        return false;
309
                    }
310
311 1
                    continue;
312
                }
313
314
                // store array of nested association
315 1
                foreach ($assocValue as $aVal) {
316
                    // Cancel put result if association put fail
317 1
                    if ($this->storeAssociationCache($key, $association, $aVal) === null) {
318 48
                        return false;
319
                    }
320
                }
321
            }
322
        }
323
324 48
        return $this->region->put($key, new QueryCacheEntry($data));
325
    }
326
327
    /**
328
     * @param AssociationMetadata $assoc
329
     * @param mixed[]             $assocValue
330
     *
331
     * @return mixed[]|null
332
     */
333 14
    private function storeAssociationCache(QueryCacheKey $key, AssociationMetadata $association, $assocValue)
334
    {
335 14
        $unitOfWork     = $this->em->getUnitOfWork();
336 14
        $assocPersister = $unitOfWork->getEntityPersister($association->getTargetEntity());
337 14
        $assocMetadata  = $assocPersister->getClassMetadata();
338 14
        $assocRegion    = $assocPersister->getCacheRegion();
339
340
        // Handle *-to-one associations
341 14
        if ($association instanceof ToOneAssociationMetadata) {
342 8
            $assocIdentifier = $unitOfWork->getEntityIdentifier($assocValue);
0 ignored issues
show
$assocValue of type array<mixed,mixed> is incompatible with the type object expected by parameter $entity of Doctrine\ORM\UnitOfWork::getEntityIdentifier(). ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

342
            $assocIdentifier = $unitOfWork->getEntityIdentifier(/** @scrutinizer ignore-type */ $assocValue);
Loading history...
343 8
            $entityKey       = new EntityCacheKey($assocMetadata->getRootClassName(), $assocIdentifier);
344
345 8
            if ((! $assocValue instanceof GhostObjectInterface && ($key->cacheMode & Cache::MODE_REFRESH)) || ! $assocRegion->contains($entityKey)) {
346
                // Entity put fail
347 7
                if (! $assocPersister->storeEntityCache($assocValue, $entityKey)) {
348 1
                    return null;
349
                }
350
            }
351
352
            return [
353 7
                'targetEntity'  => $assocMetadata->getRootClassName(),
354 7
                'identifier'    => $assocIdentifier,
355
            ];
356
        }
357
358
        // Handle *-to-many associations
359 6
        $list = [];
360
361 6
        foreach ($assocValue as $assocItemIndex => $assocItem) {
362 6
            $assocIdentifier = $unitOfWork->getEntityIdentifier($assocItem);
363 6
            $entityKey       = new EntityCacheKey($assocMetadata->getRootClassName(), $assocIdentifier);
364
365 6
            if (($key->cacheMode & Cache::MODE_REFRESH) || ! $assocRegion->contains($entityKey)) {
366
                // Entity put fail
367 6
                if (! $assocPersister->storeEntityCache($assocItem, $entityKey)) {
368 1
                    return null;
369
                }
370
            }
371
372 5
            $list[$assocItemIndex] = $assocIdentifier;
373
        }
374
375
        return [
376 5
            'targetEntity' => $assocMetadata->getRootClassName(),
377 5
            'list'         => $list,
378
        ];
379
    }
380
381
    /**
382
     * @param string $assocAlias
383
     * @param object $entity
384
     *
385
     * @return mixed[]|object
386
     */
387 16
    private function getAssociationValue(ResultSetMapping $rsm, $assocAlias, $entity)
388
    {
389 16
        $path  = [];
390 16
        $alias = $assocAlias;
391
392 16
        while (isset($rsm->parentAliasMap[$alias])) {
393 16
            $parent = $rsm->parentAliasMap[$alias];
394 16
            $field  = $rsm->relationMap[$alias];
395 16
            $class  = $rsm->aliasMap[$parent];
396
397 16
            array_unshift($path, [
398 16
                'field'  => $field,
399 16
                'class'  => $class,
400
            ]);
401
402 16
            $alias = $parent;
403
        }
404
405 16
        return $this->getAssociationPathValue($entity, $path);
406
    }
407
408
    /**
409
     * @param mixed     $value
410
     * @param mixed[][] $path
411
     *
412
     * @return mixed[]|object|null
413
     */
414 16
    private function getAssociationPathValue($value, array $path)
415
    {
416 16
        $mapping     = array_shift($path);
417 16
        $metadata    = $this->em->getClassMetadata($mapping['class']);
418 16
        $association = $metadata->getProperty($mapping['field']);
419 16
        $value       = $association->getValue($value);
420
421 16
        if ($value === null) {
422 1
            return null;
423
        }
424
425 15
        if (empty($path)) {
426 15
            return $value;
427
        }
428
429
        // Handle *-to-one associations
430 3
        if ($association instanceof ToOneAssociationMetadata) {
431 1
            return $this->getAssociationPathValue($value, $path);
432
        }
433
434 2
        $values = [];
435
436 2
        foreach ($value as $item) {
437 2
            $values[] = $this->getAssociationPathValue($item, $path);
438
        }
439
440 2
        return $values;
441
    }
442
443
    /**
444
     * {@inheritdoc}
445
     */
446 48
    public function clear()
447
    {
448 48
        return $this->region->evictAll();
449
    }
450
451
    /**
452
     * {@inheritdoc}
453
     */
454 28
    public function getRegion()
455
    {
456 28
        return $this->region;
457
    }
458
}
459