Passed
Pull Request — master (#7681)
by
unknown
11:38
created

DefaultQueryCache::storeAssociationCache()   B

Complexity

Conditions 10
Paths 7

Size

Total Lines 45
Code Lines 24

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 110

Importance

Changes 0
Metric Value
cc 10
eloc 24
nc 7
nop 3
dl 0
loc 45
ccs 0
cts 23
cp 0
crap 110
rs 7.6666
c 0
b 0
f 0

How to fix   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 71
    public function __construct(EntityManagerInterface $em, Region $region)
51
    {
52 71
        $cacheConfig = $em->getConfiguration()->getSecondLevelCacheConfiguration();
53
54 71
        $this->em          = $em;
55 71
        $this->region      = $region;
56 71
        $this->cacheLogger = $cacheConfig->getCacheLogger();
57 71
        $this->validator   = $cacheConfig->getQueryValidator();
58 71
    }
59
60
    /**
61
     * {@inheritdoc}
62
     */
63 43
    public function get(QueryCacheKey $key, ResultSetMapping $rsm, array $hints = [])
64
    {
65 43
        if (! ($key->cacheMode & Cache::MODE_GET)) {
66 3
            return null;
67
        }
68
69 42
        $cacheEntry = $this->region->get($key);
70
71 42
        if (! $cacheEntry instanceof QueryCacheEntry) {
72 38
            return null;
73
        }
74
75 29
        if (! $this->validator->isValid($key, $cacheEntry)) {
76 8
            $this->region->evict($key);
77
78 8
            return null;
79
        }
80
81 24
        $result      = [];
82 24
        $entityName  = reset($rsm->aliasMap);
83 24
        $hasRelation = ! empty($rsm->relationMap);
84 24
        $unitOfWork  = $this->em->getUnitOfWork();
85 24
        $persister   = $unitOfWork->getEntityPersister($entityName);
86 24
        $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 24
        $regionName  = $region->getName();
88
89 24
        $cm = $this->em->getClassMetadata($entityName);
90
91
        $generateKeys = static function (array $entry) use ($cm) : EntityCacheKey {
92 24
            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 24
        };
94
95 24
        $cacheKeys = new CollectionCacheEntry(array_map($generateKeys, $cacheEntry->result));
96 24
        $entries   = $region->getMultiple($cacheKeys);
97
98
        // @TODO - move to cache hydration component
99 24
        foreach ($cacheEntry->result as $index => $entry) {
100 24
            $entityEntry = is_array($entries) ? ($entries[$index] ?? null) : null;
101
102 24
            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 22
            if ($this->cacheLogger !== null) {
111 20
                $this->cacheLogger->entityCacheHit($regionName, $cacheKeys->identifiers[$index]);
112
            }
113
114 22
            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
            $data = $entityEntry->data;
125
126
            foreach ($entry['associations'] as $name => $assoc) {
127
                $assocPersister = $unitOfWork->getEntityPersister($assoc['targetEntity']);
128
                $assocRegion    = $assocPersister->getCacheRegion();
129
                $assocMetadata  = $this->em->getClassMetadata($assoc['targetEntity']);
130
131
                // *-to-one association
132
                if (isset($assoc['identifier'])) {
133
                    $assocKey = new EntityCacheKey($assocMetadata->getRootClassName(), $assoc['identifier']);
134
135
                    $assocEntry = $assocRegion->get($assocKey);
136
                    if ($assocEntry === null) {
137
                        if ($this->cacheLogger !== null) {
138
                            $this->cacheLogger->entityCacheMiss($assocRegion->getName(), $assocKey);
139
                        }
140
141
                        $unitOfWork->hydrationComplete();
142
143
                        return null;
144
                    }
145
146
                    $data[$name] = $unitOfWork->createEntity(
147
                        $assocEntry->class,
148
                        $assocEntry->resolveAssociationEntries($this->em),
149
                        self::$hints
150
                    );
151
152
                    if ($this->cacheLogger !== null) {
153
                        $this->cacheLogger->entityCacheHit($assocRegion->getName(), $assocKey);
154
                    }
155
156
                    continue;
157
                }
158
159
                if (! isset($assoc['list']) || empty($assoc['list'])) {
160
                    continue;
161
                }
162
163
                $generateKeys = static function ($id) use ($assocMetadata) : EntityCacheKey {
164
                    return new EntityCacheKey($assocMetadata->getRootClassName(), $id);
165
                };
166
167
                $assocKeys    = new CollectionCacheEntry(array_map($generateKeys, $assoc['list']));
168
                $assocEntries = $assocRegion->getMultiple($assocKeys);
169
170
                // *-to-many association
171
                $collection = [];
172
173
                foreach ($assoc['list'] as $assocIndex => $assocId) {
174
                    $assocEntry = is_array($assocEntries) ? ($assocEntries[$assocIndex] ?? null) : null;
175
176
                    if ($assocEntry === null) {
177
                        if ($this->cacheLogger !== null) {
178
                            $this->cacheLogger->entityCacheMiss($assocRegion->getName(), $assocKeys->identifiers[$assocIndex]);
179
                        }
180
181
                        $unitOfWork->hydrationComplete();
182
183
                        return null;
184
                    }
185
186
                    $collection[$assocIndex] = $unitOfWork->createEntity(
187
                        $assocEntry->class,
188
                        $assocEntry->resolveAssociationEntries($this->em),
189
                        self::$hints
190
                    );
191
192
                    if ($this->cacheLogger !== null) {
193
                        $this->cacheLogger->entityCacheHit($assocRegion->getName(), $assocKeys->identifiers[$assocIndex]);
194
                    }
195
                }
196
197
                $data[$name] = $collection;
198
            }
199
200
            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
                if ($unCachedAssociationData instanceof AssociationCacheEntry) {
212
                    $data[$fieldName] = $this->em->getReference(
213
                        $unCachedAssociationData->class,
214
                        $unCachedAssociationData->identifier
215
                    );
216
                }
217
            }
218
219
            $result[$index] = $unitOfWork->createEntity($entityEntry->class, $data, self::$hints);
220
        }
221
222 22
        $unitOfWork->hydrationComplete();
223
224 22
        return $result;
225
    }
226
227
    /**
228
     * {@inheritdoc}
229
     *
230
     * @param mixed[] $hints
231
     */
232 43
    public function put(QueryCacheKey $key, ResultSetMapping $rsm, $result, array $hints = [])
233
    {
234 43
        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 42
        if (count($rsm->entityMappings) > 1) {
239 1
            throw FeatureNotImplemented::multipleRootEntities();
240
        }
241
242 41
        if (! $rsm->isSelect) {
243 2
            throw FeatureNotImplemented::nonSelectStatements();
244
        }
245
246 39
        if (isset($hints[Query::HINT_FORCE_PARTIAL_LOAD]) && $hints[Query::HINT_FORCE_PARTIAL_LOAD]) {
247 1
            throw FeatureNotImplemented::partialEntities();
248
        }
249
250 38
        if (! ($key->cacheMode & Cache::MODE_PUT)) {
251 3
            return false;
252
        }
253
254 37
        $data       = [];
255 37
        $entityName = reset($rsm->aliasMap);
256 37
        $rootAlias  = key($rsm->aliasMap);
257 37
        $unitOfWork = $this->em->getUnitOfWork();
258 37
        $persister  = $unitOfWork->getEntityPersister($entityName);
259
260 37
        if (! ($persister instanceof CachedPersister)) {
261 1
            throw NonCacheableEntity::fromEntity($entityName);
262
        }
263
264 36
        $region = $persister->getCacheRegion();
265
266 36
        foreach ($result as $index => $entity) {
267 36
            $identifier = $unitOfWork->getEntityIdentifier($entity);
268 36
            $entityKey  = new EntityCacheKey($entityName, $identifier);
269
270 36
            if (($key->cacheMode & Cache::MODE_REFRESH) || ! $region->contains($entityKey)) {
271
                // Cancel put result if entity put fail
272 22
                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 35
            $data[$index]['identifier']   = $identifier;
278 35
            $data[$index]['associations'] = [];
279
280
            // @TODO - move to cache hydration components
281 35
            foreach ($rsm->relationMap as $alias => $name) {
282
                $parentAlias = $rsm->parentAliasMap[$alias];
283
                $parentClass = $rsm->aliasMap[$parentAlias];
284
                $metadata    = $this->em->getClassMetadata($parentClass);
285
                $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

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
                $assocValue  = $this->getAssociationValue($rsm, $alias, $entity);
287
288
                if ($assocValue === null) {
289
                    continue;
290
                }
291
292
                // root entity association
293
                if ($rootAlias === $parentAlias) {
294
                    // Cancel put result if association put fail
295
                    $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
                    if ($assocInfo === null) {
297
                        return false;
298
                    }
299
300
                    $data[$index]['associations'][$name] = $assocInfo;
301
302
                    continue;
303
                }
304
305
                // store single nested association
306
                if (! is_array($assocValue)) {
307
                    // Cancel put result if association put fail
308
                    if ($this->storeAssociationCache($key, $association, $assocValue) === null) {
309
                        return false;
310
                    }
311
312
                    continue;
313
                }
314
315
                // store array of nested association
316
                foreach ($assocValue as $aVal) {
317
                    // Cancel put result if association put fail
318
                    if ($this->storeAssociationCache($key, $association, $aVal) === null) {
319
                        return false;
320
                    }
321
                }
322
            }
323
        }
324
325 35
        return $this->region->put($key, new QueryCacheEntry($data));
326
    }
327
328
    /**
329
     * @param mixed[] $assocValue
330
     *
331
     * @return mixed[]|null
332
     */
333
    private function storeAssociationCache(QueryCacheKey $key, AssociationMetadata $association, $assocValue)
334
    {
335
        $unitOfWork     = $this->em->getUnitOfWork();
336
        $assocPersister = $unitOfWork->getEntityPersister($association->getTargetEntity());
337
        $assocMetadata  = $assocPersister->getClassMetadata();
338
        $assocRegion    = $assocPersister->getCacheRegion();
339
340
        // Handle *-to-one associations
341
        if ($association instanceof ToOneAssociationMetadata) {
342
            $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
            $entityKey       = new EntityCacheKey($assocMetadata->getRootClassName(), $assocIdentifier);
344
345
            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
                if (! $assocPersister->storeEntityCache($assocValue, $entityKey)) {
348
                    return null;
349
                }
350
            }
351
352
            return [
353
                'targetEntity'  => $assocMetadata->getRootClassName(),
354
                'identifier'    => $assocIdentifier,
355
            ];
356
        }
357
358
        // Handle *-to-many associations
359
        $list = [];
360
361
        foreach ($assocValue as $assocItemIndex => $assocItem) {
362
            $assocIdentifier = $unitOfWork->getEntityIdentifier($assocItem);
363
            $entityKey       = new EntityCacheKey($assocMetadata->getRootClassName(), $assocIdentifier);
364
365
            if (($key->cacheMode & Cache::MODE_REFRESH) || ! $assocRegion->contains($entityKey)) {
366
                // Entity put fail
367
                if (! $assocPersister->storeEntityCache($assocItem, $entityKey)) {
368
                    return null;
369
                }
370
            }
371
372
            $list[$assocItemIndex] = $assocIdentifier;
373
        }
374
375
        return [
376
            'targetEntity' => $assocMetadata->getRootClassName(),
377
            'list'         => $list,
378
        ];
379
    }
380
381
    /**
382
     * @param string $assocAlias
383
     * @param object $entity
384
     *
385
     * @return mixed[]|object
386
     */
387 1
    private function getAssociationValue(ResultSetMapping $rsm, $assocAlias, $entity)
388
    {
389 1
        $path  = [];
390 1
        $alias = $assocAlias;
391
392 1
        while (isset($rsm->parentAliasMap[$alias])) {
393 1
            $parent = $rsm->parentAliasMap[$alias];
394 1
            $field  = $rsm->relationMap[$alias];
395 1
            $class  = $rsm->aliasMap[$parent];
396
397 1
            array_unshift($path, [
398 1
                'field'  => $field,
399 1
                'class'  => $class,
400
            ]);
401
402 1
            $alias = $parent;
403
        }
404
405 1
        return $this->getAssociationPathValue($entity, $path);
406
    }
407
408
    /**
409
     * @param mixed     $value
410
     * @param mixed[][] $path
411
     *
412
     * @return mixed[]|object|null
413
     */
414 1
    private function getAssociationPathValue($value, array $path)
415
    {
416 1
        $mapping     = array_shift($path);
417 1
        $metadata    = $this->em->getClassMetadata($mapping['class']);
418 1
        $association = $metadata->getProperty($mapping['field']);
419 1
        $value       = $association->getValue($value);
420
421 1
        if ($value === null) {
422
            return null;
423
        }
424
425 1
        if (empty($path)) {
426 1
            return $value;
427
        }
428
429
        // Handle *-to-one associations
430 1
        if ($association instanceof ToOneAssociationMetadata) {
431
            return $this->getAssociationPathValue($value, $path);
432
        }
433
434 1
        $values = [];
435
436 1
        foreach ($value as $item) {
437 1
            $values[] = $this->getAssociationPathValue($item, $path);
438
        }
439
440 1
        return $values;
441
    }
442
443
    /**
444
     * {@inheritdoc}
445
     */
446 40
    public function clear()
447
    {
448 40
        return $this->region->evictAll();
449
    }
450
451
    /**
452
     * {@inheritdoc}
453
     */
454 20
    public function getRegion()
455
    {
456 20
        return $this->region;
457
    }
458
}
459