Failed Conditions
Push — master ( 901929...f6cc12 )
by Jonathan
11:18
created

DefaultQueryCache::put()   D

Complexity

Conditions 20
Paths 24

Size

Total Lines 94
Code Lines 48

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 48
CRAP Score 20.0033

Importance

Changes 0
Metric Value
cc 20
eloc 48
nc 24
nop 4
dl 0
loc 94
ccs 48
cts 49
cp 0.9796
crap 20.0033
rs 4.7294
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 = 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\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
                $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 FeatureNotImplemented::scalarResults();
235
        }
236
237 57
        if (count($rsm->entityMappings) > 1) {
238 1
            throw FeatureNotImplemented::multipleRootEntities();
239
        }
240
241 56
        if (! $rsm->isSelect) {
242 2
            throw FeatureNotImplemented::nonSelectStatements();
243
        }
244
245 54
        if (isset($hints[Query::HINT_FORCE_PARTIAL_LOAD]) && $hints[Query::HINT_FORCE_PARTIAL_LOAD]) {
246 1
            throw FeatureNotImplemented::partialEntities();
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 NonCacheableEntity::fromEntity($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
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

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

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
Bug introduced by
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
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

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 mixed[] $assocValue
329
     *
330
     * @return mixed[]|null
331
     */
332 14
    private function storeAssociationCache(QueryCacheKey $key, AssociationMetadata $association, $assocValue)
333
    {
334 14
        $unitOfWork     = $this->em->getUnitOfWork();
335 14
        $assocPersister = $unitOfWork->getEntityPersister($association->getTargetEntity());
336 14
        $assocMetadata  = $assocPersister->getClassMetadata();
337 14
        $assocRegion    = $assocPersister->getCacheRegion();
338
339
        // Handle *-to-one associations
340 14
        if ($association instanceof ToOneAssociationMetadata) {
341 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

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