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

DefaultQueryCache::get()   D

Complexity

Conditions 24
Paths 106

Size

Total Lines 153
Code Lines 75

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 75
CRAP Score 24.0013

Importance

Changes 3
Bugs 0 Features 0
Metric Value
cc 24
eloc 75
c 3
b 0
f 0
nop 3
dl 0
loc 153
ccs 75
cts 76
cp 0.9868
crap 24.0013
rs 4.1166
nc 106

How to fix   Long Method    Complexity   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

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