DefaultQueryCache::get()   F
last analyzed

Complexity

Conditions 24
Paths 144

Size

Total Lines 162
Code Lines 83

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 83
CRAP Score 24.0009

Importance

Changes 0
Metric Value
cc 24
eloc 83
nc 144
nop 3
dl 0
loc 162
ccs 83
cts 84
cp 0.9881
crap 24.0009
rs 3.8
c 0
b 0
f 0

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
declare(strict_types=1);
4
5
namespace Doctrine\ORM\Cache;
6
7
use Doctrine\ORM\Cache;
8
use Doctrine\ORM\Cache\Exception\FeatureNotImplemented;
9
use Doctrine\ORM\Cache\Exception\NonCacheableEntity;
10
use Doctrine\ORM\Cache\Logging\CacheLogger;
11
use Doctrine\ORM\Cache\Persister\CachedPersister;
12
use Doctrine\ORM\EntityManagerInterface;
13
use Doctrine\ORM\Mapping\AssociationMetadata;
14
use Doctrine\ORM\Mapping\ToOneAssociationMetadata;
15
use Doctrine\ORM\Query;
16
use Doctrine\ORM\Query\ResultSetMapping;
17
use ProxyManager\Proxy\GhostObjectInterface;
18
use function array_map;
19
use function array_shift;
20
use function array_unshift;
21
use function count;
22
use function is_array;
23
use function key;
24
use function reset;
25
26
/**
27
 * Default query cache implementation.
28
 */
29
class DefaultQueryCache implements QueryCache
30
{
31
    /** @var EntityManagerInterface */
32
    private $em;
33
34
    /** @var Region */
35
    private $region;
36
37
    /** @var QueryCacheValidator */
38
    private $validator;
39
40
    /** @var CacheLogger */
41
    protected $cacheLogger;
42
43
    /** @var mixed[] */
44
    private static $hints = [Query::HINT_CACHE_ENABLED => true];
45
46
    /**
47
     * @param EntityManagerInterface $em     The entity manager.
48
     * @param 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
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

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
        $generateKeys = static function (array $entry) use ($cm) : EntityCacheKey {
92 33
            return new EntityCacheKey($cm->getRootClassName(), $entry['identifier']);
0 ignored issues
show
Bug introduced by
The method getRootClassName() does not exist on Doctrine\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
                $generateKeys = static 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 4
                        $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
     *
230
     * @param mixed[] $hints
231
     */
232 58
    public function put(QueryCacheKey $key, ResultSetMapping $rsm, $result, array $hints = [])
233
    {
234 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...
235 1
            throw FeatureNotImplemented::scalarResults();
236
        }
237
238 57
        if (count($rsm->entityMappings) > 1) {
239 1
            throw FeatureNotImplemented::multipleRootEntities();
240
        }
241
242 56
        if (! $rsm->isSelect) {
243 2
            throw FeatureNotImplemented::nonSelectStatements();
244
        }
245
246 54
        if (isset($hints[Query::HINT_FORCE_PARTIAL_LOAD]) && $hints[Query::HINT_FORCE_PARTIAL_LOAD]) {
247 1
            throw FeatureNotImplemented::partialEntities();
248
        }
249
250 53
        if (! ($key->cacheMode & Cache::MODE_PUT)) {
251 3
            return false;
252
        }
253
254 52
        $data       = [];
255 52
        $entityName = reset($rsm->aliasMap);
256 52
        $rootAlias  = key($rsm->aliasMap);
257 52
        $unitOfWork = $this->em->getUnitOfWork();
258 52
        $persister  = $unitOfWork->getEntityPersister($entityName);
259
260 52
        if (! ($persister instanceof CachedPersister)) {
261 1
            throw NonCacheableEntity::fromEntity($entityName);
262
        }
263
264 51
        $region = $persister->getCacheRegion();
265
266 51
        foreach ($result as $index => $entity) {
267 51
            $identifier = $unitOfWork->getEntityIdentifier($entity);
268 51
            $entityKey  = new EntityCacheKey($entityName, $identifier);
269
270 51
            if (($key->cacheMode & Cache::MODE_REFRESH) || ! $region->contains($entityKey)) {
271
                // Cancel put result if entity put fail
272 36
                if (! $persister->storeEntityCache($entity, $entityKey)) {
0 ignored issues
show
Bug introduced by
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

272
                if (! $persister->/** @scrutinizer ignore-call */ storeEntityCache($entity, $entityKey)) {
Loading history...
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

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

285
                /** @scrutinizer ignore-call */ 
286
                $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...
286 15
                $assocValue  = $this->getAssociationValue($rsm, $alias, $entity);
287
288 15
                if ($assocValue === null) {
289 1
                    continue;
290
                }
291
292
                // root entity association
293 14
                if ($rootAlias === $parentAlias) {
294
                    // Cancel put result if association put fail
295 14
                    $assocInfo = $this->storeAssociationCache($key, $association, $assocValue);
0 ignored issues
show
Bug introduced by
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

295
                    $assocInfo = $this->storeAssociationCache($key, $association, /** @scrutinizer ignore-type */ $assocValue);
Loading history...
296 14
                    if ($assocInfo === null) {
297 2
                        return false;
298
                    }
299
300 12
                    $data[$index]['associations'][$name] = $assocInfo;
301
302 12
                    continue;
303
                }
304
305
                // store single nested association
306 2
                if (! is_array($assocValue)) {
307
                    // Cancel put result if association put fail
308 1
                    if ($this->storeAssociationCache($key, $association, $assocValue) === null) {
309
                        return false;
310
                    }
311
312 1
                    continue;
313
                }
314
315
                // store array of nested association
316 1
                foreach ($assocValue as $aVal) {
317
                    // Cancel put result if association put fail
318 1
                    if ($this->storeAssociationCache($key, $association, $aVal) === null) {
319
                        return false;
320
                    }
321
                }
322
            }
323
        }
324
325 48
        return $this->region->put($key, new QueryCacheEntry($data));
326
    }
327
328
    /**
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
Bug introduced by
$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)) {
0 ignored issues
show
introduced by
$assocValue is never a sub-type of ProxyManager\Proxy\GhostObjectInterface.
Loading history...
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