Passed
Pull Request — 2.8.x (#8009)
by Peter
08:31
created

DefaultQueryCache::clear()   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
cc 1
eloc 1
nc 1
nop 0
dl 0
loc 3
ccs 2
cts 2
cp 1
crap 1
rs 10
c 0
b 0
f 0
1
<?php
2
3
/*
4
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
5
 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
6
 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
7
 * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
8
 * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
9
 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
10
 * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
11
 * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
12
 * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
13
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
14
 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
15
 *
16
 * This software consists of voluntary contributions made by many individuals
17
 * and is licensed under the MIT license. For more information, see
18
 * <http://www.doctrine-project.org>.
19
 */
20
21
namespace Doctrine\ORM\Cache;
22
23
use Doctrine\Common\Collections\ArrayCollection;
24
use Doctrine\ORM\Cache\Persister\CachedPersister;
25
use Doctrine\ORM\Cache\Persister\Entity\CachedEntityPersister;
26
use Doctrine\ORM\EntityManagerInterface;
27
use Doctrine\ORM\Query\ResultSetMapping;
28
use Doctrine\ORM\Mapping\ClassMetadata;
29
use Doctrine\ORM\PersistentCollection;
30
use Doctrine\Common\Proxy\Proxy;
31
use Doctrine\ORM\Cache;
32
use Doctrine\ORM\Query;
33
use function assert;
34
35
/**
36
 * Default query cache implementation.
37
 *
38
 * @since   2.5
39
 * @author  Fabio B. Silva <[email protected]>
40
 */
41
class DefaultQueryCache implements QueryCache
42
{
43
     /**
44
     * @var \Doctrine\ORM\EntityManagerInterface
45
     */
46
    private $em;
47
48
    /**
49
     * @var \Doctrine\ORM\UnitOfWork
50
     */
51
    private $uow;
52
53
    /**
54
     * @var \Doctrine\ORM\Cache\Region
55
     */
56
    private $region;
57
58
    /**
59
     * @var \Doctrine\ORM\Cache\QueryCacheValidator
60
     */
61
    private $validator;
62
63
    /**
64
     * @var \Doctrine\ORM\Cache\Logging\CacheLogger
65
     */
66
    protected $cacheLogger;
67
68
    /**
69
     * @var array
70
     */
71
    private static $hints = [Query::HINT_CACHE_ENABLED => true];
72
73
    /**
74
     * @param \Doctrine\ORM\EntityManagerInterface $em     The entity manager.
75
     * @param \Doctrine\ORM\Cache\Region           $region The query region.
76
     */
77 88
    public function __construct(EntityManagerInterface $em, Region $region)
78
    {
79 88
        $cacheConfig = $em->getConfiguration()->getSecondLevelCacheConfiguration();
80
81 88
        $this->em           = $em;
82 88
        $this->region       = $region;
83 88
        $this->uow          = $em->getUnitOfWork();
84 88
        $this->cacheLogger  = $cacheConfig->getCacheLogger();
85 88
        $this->validator    = $cacheConfig->getQueryValidator();
86 88
    }
87
88
    /**
89
     * {@inheritdoc}
90
     */
91 54
    public function get(QueryCacheKey $key, ResultSetMapping $rsm, array $hints = [])
92
    {
93 54
        if ( ! ($key->cacheMode & Cache::MODE_GET)) {
94 3
            return null;
95
        }
96
97 53
        $cacheEntry = $this->region->get($key);
98
99 53
        if ( ! $cacheEntry instanceof QueryCacheEntry) {
100 48
            return null;
101
        }
102
103 40
        if ( ! $this->validator->isValid($key, $cacheEntry)) {
104 8
            $this->region->evict($key);
105
106 8
            return null;
107
        }
108
109 35
        $result      = [];
110 35
        $entityName  = reset($rsm->aliasMap);
111 35
        $hasRelation = ! empty($rsm->relationMap);
112 35
        $persister   = $this->uow->getEntityPersister($entityName);
113 35
        assert($persister instanceof CachedEntityPersister);
114
115 35
        $region     = $persister->getCacheRegion();
116 35
        $regionName = $region->getName();
117
118 35
        $cm = $this->em->getClassMetadata($entityName);
119
120
        $generateKeys = static function (array $entry) use ($cm) : EntityCacheKey {
121 35
            return new EntityCacheKey($cm->rootEntityName, $entry['identifier']);
0 ignored issues
show
Bug introduced by
Accessing rootEntityName on the interface Doctrine\Common\Persistence\Mapping\ClassMetadata suggest that you code against a concrete implementation. How about adding an instanceof check?
Loading history...
122 35
        };
123
124 35
        $cacheKeys = new CollectionCacheEntry(array_map($generateKeys, $cacheEntry->result));
125 35
        $entries   = $region->getMultiple($cacheKeys) ?? [];
126
127
        // @TODO - move to cache hydration component
128 35
        foreach ($cacheEntry->result as $index => $entry) {
129 35
            $entityEntry = $entries[$index] ?? null;
130
131 35
            if (! $entityEntry instanceof EntityCacheEntry) {
132 3
                if ($this->cacheLogger !== null) {
133 1
                    $this->cacheLogger->entityCacheMiss($regionName, $cacheKeys->identifiers[$index]);
134
                }
135
136 3
                return null;
137
            }
138
139 33
            if ($this->cacheLogger !== null) {
140 30
                $this->cacheLogger->entityCacheHit($regionName, $cacheKeys->identifiers[$index]);
141
            }
142
143 33
            if ( ! $hasRelation) {
144 24
                $result[$index]  = $this->uow->createEntity($entityEntry->class, $entityEntry->resolveAssociationEntries($this->em), self::$hints);
145
146 24
                continue;
147
            }
148
149 9
            $data = $entityEntry->data;
150
151 9
            foreach ($entry['associations'] as $name => $assoc) {
152 9
                $assocPersister = $this->uow->getEntityPersister($assoc['targetEntity']);
153 9
                assert($assocPersister instanceof CachedEntityPersister);
154
155 9
                $assocRegion   = $assocPersister->getCacheRegion();
156 9
                $assocMetadata = $this->em->getClassMetadata($assoc['targetEntity']);
157
158 9
                if ($assoc['type'] & ClassMetadata::TO_ONE) {
159
160 5
                    if (($assocEntry = $assocRegion->get($assocKey = new EntityCacheKey($assocMetadata->rootEntityName, $assoc['identifier']))) === null) {
0 ignored issues
show
Bug introduced by
Accessing rootEntityName on the interface Doctrine\Common\Persistence\Mapping\ClassMetadata suggest that you code against a concrete implementation. How about adding an instanceof check?
Loading history...
161
162 1
                        if ($this->cacheLogger !== null) {
163 1
                            $this->cacheLogger->entityCacheMiss($assocRegion->getName(), $assocKey);
164
                        }
165
166 1
                        $this->uow->hydrationComplete();
167
168 1
                        return null;
169
                    }
170
171 4
                    $data[$name] = $this->uow->createEntity($assocEntry->class, $assocEntry->resolveAssociationEntries($this->em), self::$hints);
0 ignored issues
show
Bug introduced by
Accessing class on the interface Doctrine\ORM\Cache\CacheEntry suggest that you code against a concrete implementation. How about adding an instanceof check?
Loading history...
Bug introduced by
The method resolveAssociationEntries() does not exist on Doctrine\ORM\Cache\CacheEntry. It seems like you code against a sub-type of Doctrine\ORM\Cache\CacheEntry such as Doctrine\ORM\Cache\EntityCacheEntry. ( Ignorable by Annotation )

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

171
                    $data[$name] = $this->uow->createEntity($assocEntry->class, $assocEntry->/** @scrutinizer ignore-call */ resolveAssociationEntries($this->em), self::$hints);
Loading history...
172
173 4
                    if ($this->cacheLogger !== null) {
174 4
                        $this->cacheLogger->entityCacheHit($assocRegion->getName(), $assocKey);
175
                    }
176
177 4
                    continue;
178
                }
179
180 4
                if ( ! isset($assoc['list']) || empty($assoc['list'])) {
181
                    continue;
182
                }
183
184
                $generateKeys = function ($id) use ($assocMetadata): EntityCacheKey {
185 4
                    return new EntityCacheKey($assocMetadata->rootEntityName, $id);
0 ignored issues
show
Bug introduced by
Accessing rootEntityName on the interface Doctrine\Common\Persistence\Mapping\ClassMetadata suggest that you code against a concrete implementation. How about adding an instanceof check?
Loading history...
186 4
                };
187
188 4
                $collection   = new PersistentCollection($this->em, $assocMetadata, new ArrayCollection());
189 4
                $assocKeys    = new CollectionCacheEntry(array_map($generateKeys, $assoc['list']));
190 4
                $assocEntries = $assocRegion->getMultiple($assocKeys);
191
192 4
                foreach ($assoc['list'] as $assocIndex => $assocId) {
193 4
                    $assocEntry = is_array($assocEntries) && array_key_exists($assocIndex, $assocEntries) ? $assocEntries[$assocIndex] : null;
194
195 4
                    if ($assocEntry === null) {
196 1
                        if ($this->cacheLogger !== null) {
197 1
                            $this->cacheLogger->entityCacheMiss($assocRegion->getName(), $assocKeys->identifiers[$assocIndex]);
198
                        }
199
200 1
                        $this->uow->hydrationComplete();
201
202 1
                        return null;
203
                    }
204
205 3
                    $element = $this->uow->createEntity($assocEntry->class, $assocEntry->resolveAssociationEntries($this->em), self::$hints);
206
207 3
                    $collection->hydrateSet($assocIndex, $element);
208
209 3
                    if ($this->cacheLogger !== null) {
210 3
                        $this->cacheLogger->entityCacheHit($assocRegion->getName(), $assocKeys->identifiers[$assocIndex]);
211
                    }
212
                }
213
214 3
                $data[$name] = $collection;
215
216 3
                $collection->setInitialized(true);
217
            }
218
219 7
            foreach ($data as $fieldName => $unCachedAssociationData) {
220
                // In some scenarios, such as EAGER+ASSOCIATION+ID+CACHE, the
221
                // cache key information in `$cacheEntry` will not contain details
222
                // for fields that are associations.
223
                //
224
                // This means that `$data` keys for some associations that may
225
                // actually not be cached will not be converted to actual association
226
                // data, yet they contain L2 cache AssociationCacheEntry objects.
227
                //
228
                // We need to unwrap those associations into proxy references,
229
                // since we don't have actual data for them except for identifiers.
230 7
                if ($unCachedAssociationData instanceof AssociationCacheEntry) {
231 4
                    $data[$fieldName] = $this->em->getReference(
232 4
                        $unCachedAssociationData->class,
233 7
                        $unCachedAssociationData->identifier
234
                    );
235
                }
236
            }
237
238 7
            $result[$index] = $this->uow->createEntity($entityEntry->class, $data, self::$hints);
239
        }
240
241 30
        $this->uow->hydrationComplete();
242
243 30
        return $result;
244
    }
245
246
    /**
247
     * {@inheritdoc}
248
     */
249 59
    public function put(QueryCacheKey $key, ResultSetMapping $rsm, $result, array $hints = [])
250
    {
251 59
        if ($rsm->scalarMappings) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $rsm->scalarMappings of type array 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...
252 1
            throw new CacheException("Second level cache does not support scalar results.");
253
        }
254
255 58
        if (count($rsm->entityMappings) > 1) {
256 1
            throw new CacheException("Second level cache does not support multiple root entities.");
257
        }
258
259 57
        if ( ! $rsm->isSelect) {
260 2
            throw new CacheException("Second-level cache query supports only select statements.");
261
        }
262
263 55
        if (isset($hints[Query::HINT_FORCE_PARTIAL_LOAD]) && $hints[Query::HINT_FORCE_PARTIAL_LOAD]) {
264 1
            throw new CacheException("Second level cache does not support partial entities.");
265
        }
266
267 54
        if ( ! ($key->cacheMode & Cache::MODE_PUT)) {
268 3
            return false;
269
        }
270
271 53
        $data        = [];
272 53
        $entityName  = reset($rsm->aliasMap);
273 53
        $rootAlias   = key($rsm->aliasMap);
274 53
        $persister   = $this->uow->getEntityPersister($entityName);
275
276 53
        if (! $persister instanceof CachedEntityPersister) {
277 1
            throw CacheException::nonCacheableEntity($entityName);
278
        }
279
280 52
        $region = $persister->getCacheRegion();
281
282 52
        $cm             = $this->em->getClassMetadata($entityName);
283 52
        $rootEntityName = $cm->getMetadataValue('rootEntityName');
284
285 52
        foreach ($result as $index => $entity) {
286 52
            $identifier = $this->uow->getEntityIdentifier($entity);
287 52
            $entityKey  = new EntityCacheKey($rootEntityName, $identifier);
288
289 52
            if (($key->cacheMode & Cache::MODE_REFRESH) || ! $region->contains($entityKey)) {
290
                // Cancel put result if entity put fail
291 36
                if ( ! $persister->storeEntityCache($entity, $entityKey)) {
292 1
                    return false;
293
                }
294
            }
295
296 51
            $data[$index]['identifier']   = $identifier;
297 51
            $data[$index]['associations'] = [];
298
299
            // @TODO - move to cache hydration components
300 51
            foreach ($rsm->relationMap as $alias => $name) {
301 15
                $parentAlias  = $rsm->parentAliasMap[$alias];
302 15
                $parentClass  = $rsm->aliasMap[$parentAlias];
303 15
                $metadata     = $this->em->getClassMetadata($parentClass);
304 15
                $assoc        = $metadata->associationMappings[$name];
0 ignored issues
show
Bug introduced by
Accessing associationMappings on the interface Doctrine\Common\Persistence\Mapping\ClassMetadata suggest that you code against a concrete implementation. How about adding an instanceof check?
Loading history...
305 15
                $assocValue   = $this->getAssociationValue($rsm, $alias, $entity);
306
307 15
                if ($assocValue === null) {
308 1
                    continue;
309
                }
310
311
                // root entity association
312 14
                if ($rootAlias === $parentAlias) {
313
                    // Cancel put result if association put fail
314 14
                    if ( ($assocInfo = $this->storeAssociationCache($key, $assoc, $assocValue)) === null) {
315 2
                        return false;
316
                    }
317
318 12
                    $data[$index]['associations'][$name] = $assocInfo;
319
320 12
                    continue;
321
                }
322
323
                // store single nested association
324 2
                if ( ! is_array($assocValue)) {
325
                    // Cancel put result if association put fail
326 1
                    if ($this->storeAssociationCache($key, $assoc, $assocValue) === null) {
327
                        return false;
328
                    }
329
330 1
                    continue;
331
                }
332
333
                // store array of nested association
334 1
                foreach ($assocValue as $aVal) {
335
                    // Cancel put result if association put fail
336 1
                    if ($this->storeAssociationCache($key, $assoc, $aVal) === null) {
337 49
                        return false;
338
                    }
339
                }
340
            }
341
        }
342
343 49
        return $this->region->put($key, new QueryCacheEntry($data));
344
    }
345
346
    /**
347
     * @param \Doctrine\ORM\Cache\QueryCacheKey $key
348
     * @param array                             $assoc
349
     * @param mixed                             $assocValue
350
     *
351
     * @return array|null
352
     */
353 14
    private function storeAssociationCache(QueryCacheKey $key, array $assoc, $assocValue)
354
    {
355 14
        $assocPersister = $this->uow->getEntityPersister($assoc['targetEntity']);
356 14
        $assocMetadata  = $assocPersister->getClassMetadata();
357 14
        $assocRegion    = $assocPersister->getCacheRegion();
0 ignored issues
show
Bug introduced by
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

357
        /** @scrutinizer ignore-call */ 
358
        $assocRegion    = $assocPersister->getCacheRegion();
Loading history...
358
359
        // Handle *-to-one associations
360 14
        if ($assoc['type'] & ClassMetadata::TO_ONE) {
361 8
            $assocIdentifier = $this->uow->getEntityIdentifier($assocValue);
362 8
            $entityKey       = new EntityCacheKey($assocMetadata->rootEntityName, $assocIdentifier);
363
364 8
            if ( ! $assocValue instanceof Proxy && ($key->cacheMode & Cache::MODE_REFRESH) || ! $assocRegion->contains($entityKey)) {
0 ignored issues
show
introduced by
Consider adding parentheses for clarity. Current Interpretation: (! $assocValue instanceo...n->contains($entityKey), Probably Intended Meaning: ! $assocValue instanceof...->contains($entityKey))
Loading history...
365
                // Entity put fail
366 7
                if ( ! $assocPersister->storeEntityCache($assocValue, $entityKey)) {
0 ignored issues
show
Bug introduced by
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

366
                if ( ! $assocPersister->/** @scrutinizer ignore-call */ storeEntityCache($assocValue, $entityKey)) {
Loading history...
367 1
                    return null;
368
                }
369
            }
370
371
            return [
372 7
                'targetEntity'  => $assocMetadata->rootEntityName,
373 7
                'identifier'    => $assocIdentifier,
374 7
                'type'          => $assoc['type']
375
            ];
376
        }
377
378
        // Handle *-to-many associations
379 6
        $list = [];
380
381 6
        foreach ($assocValue as $assocItemIndex => $assocItem) {
382 6
            $assocIdentifier = $this->uow->getEntityIdentifier($assocItem);
383 6
            $entityKey       = new EntityCacheKey($assocMetadata->rootEntityName, $assocIdentifier);
384
385 6
            if (($key->cacheMode & Cache::MODE_REFRESH) || ! $assocRegion->contains($entityKey)) {
386
                // Entity put fail
387 6
                if ( ! $assocPersister->storeEntityCache($assocItem, $entityKey)) {
388 1
                    return null;
389
                }
390
            }
391
392 5
            $list[$assocItemIndex] = $assocIdentifier;
393
        }
394
395
        return [
396 5
            'targetEntity'  => $assocMetadata->rootEntityName,
397 5
            'type'          => $assoc['type'],
398 5
            'list'          => $list,
399
        ];
400
    }
401
402
    /**
403
     * @param \Doctrine\ORM\Query\ResultSetMapping $rsm
404
     * @param string                               $assocAlias
405
     * @param object                               $entity
406
     *
407
     * @return array|object
408
     */
409 16
    private function getAssociationValue(ResultSetMapping $rsm, $assocAlias, $entity)
410
    {
411 16
        $path  = [];
412 16
        $alias = $assocAlias;
413
414 16
        while (isset($rsm->parentAliasMap[$alias])) {
415 16
            $parent = $rsm->parentAliasMap[$alias];
416 16
            $field  = $rsm->relationMap[$alias];
417 16
            $class  = $rsm->aliasMap[$parent];
418
419 16
            array_unshift($path, [
420 16
                'field'  => $field,
421 16
                'class'  => $class
422
            ]
423
            );
424
425 16
            $alias = $parent;
426
        }
427
428 16
        return $this->getAssociationPathValue($entity, $path);
429
    }
430
431
    /**
432
     * @param mixed $value
433
     * @param array $path
434
     *
435
     * @return array|object|null
436
     */
437 16
    private function getAssociationPathValue($value, array $path)
438
    {
439 16
        $mapping  = array_shift($path);
440 16
        $metadata = $this->em->getClassMetadata($mapping['class']);
441 16
        $assoc    = $metadata->associationMappings[$mapping['field']];
0 ignored issues
show
Bug introduced by
Accessing associationMappings on the interface Doctrine\Common\Persistence\Mapping\ClassMetadata suggest that you code against a concrete implementation. How about adding an instanceof check?
Loading history...
442 16
        $value    = $metadata->getFieldValue($value, $mapping['field']);
0 ignored issues
show
Bug introduced by
The method getFieldValue() does not exist on Doctrine\Common\Persistence\Mapping\ClassMetadata. Did you maybe mean getFieldNames()? ( Ignorable by Annotation )

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

442
        /** @scrutinizer ignore-call */ 
443
        $value    = $metadata->getFieldValue($value, $mapping['field']);

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...
443
444 16
        if ($value === null) {
445 1
            return null;
446
        }
447
448 15
        if (empty($path)) {
449 15
            return $value;
450
        }
451
452
        // Handle *-to-one associations
453 3
        if ($assoc['type'] & ClassMetadata::TO_ONE) {
454 1
            return $this->getAssociationPathValue($value, $path);
455
        }
456
457 2
        $values = [];
458
459 2
        foreach ($value as $item) {
460 2
            $values[] = $this->getAssociationPathValue($item, $path);
461
        }
462
463 2
        return $values;
464
    }
465
466
    /**
467
     * {@inheritdoc}
468
     */
469 48
    public function clear()
470
    {
471 48
        return $this->region->evictAll();
472
    }
473
474
    /**
475
     * {@inheritdoc}
476
     */
477 28
    public function getRegion()
478
    {
479 28
        return $this->region;
480
    }
481
}
482