Failed Conditions
Push — develop ( 3b446f...d1b453 )
by Guilherme
62:33
created

DefaultQueryCache::storeAssociationCache()   C

Complexity

Conditions 10
Paths 7

Size

Total Lines 47
Code Lines 25

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 21
CRAP Score 10

Importance

Changes 0
Metric Value
dl 0
loc 47
ccs 21
cts 21
cp 1
rs 5.1578
c 0
b 0
f 0
cc 10
eloc 25
nc 7
nop 3
crap 10

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
4
declare(strict_types=1);
5
6
namespace Doctrine\ORM\Cache;
7
8
use Doctrine\Common\Collections\ArrayCollection;
9
use Doctrine\ORM\Cache\Persister\CachedPersister;
10
use Doctrine\ORM\EntityManagerInterface;
11
use Doctrine\ORM\Mapping\AssociationMetadata;
12
use Doctrine\ORM\Mapping\ToOneAssociationMetadata;
13
use Doctrine\ORM\Query\ResultSetMapping;
14
use Doctrine\ORM\Mapping\ClassMetadata;
15
use Doctrine\ORM\PersistentCollection;
16
use Doctrine\Common\Proxy\Proxy;
17
use Doctrine\ORM\Cache;
18
use Doctrine\ORM\Query;
19
use ProxyManager\Proxy\GhostObjectInterface;
20
21
/**
22
 * Default query cache implementation.
23
 *
24
 * @since   2.5
25
 * @author  Fabio B. Silva <[email protected]>
26
 */
27
class DefaultQueryCache implements QueryCache
28
{
29
     /**
30
     * @var \Doctrine\ORM\EntityManagerInterface
31
     */
32
    private $em;
33
34
    /**
35
     * @var \Doctrine\ORM\Cache\Region
36
     */
37
    private $region;
38
39
    /**
40
     * @var \Doctrine\ORM\Cache\QueryCacheValidator
41
     */
42
    private $validator;
43
44
    /**
45
     * @var \Doctrine\ORM\Cache\Logging\CacheLogger
46
     */
47
    protected $cacheLogger;
48
49
    /**
50
     * @var array
51
     */
52
    private static $hints = [Query::HINT_CACHE_ENABLED => true];
53
54
    /**
55
     * @param \Doctrine\ORM\EntityManagerInterface $em     The entity manager.
56
     * @param \Doctrine\ORM\Cache\Region           $region The query region.
57
     */
58
    public function __construct(EntityManagerInterface $em, Region $region)
59
    {
60
        $cacheConfig = $em->getConfiguration()->getSecondLevelCacheConfiguration();
61
62
        $this->em           = $em;
63
        $this->region       = $region;
64
        $this->cacheLogger  = $cacheConfig->getCacheLogger();
65
        $this->validator    = $cacheConfig->getQueryValidator();
66
    }
67
68
    /**
69
     * {@inheritdoc}
70
     */
71
    public function get(QueryCacheKey $key, ResultSetMapping $rsm, array $hints = [])
72
    {
73
        if ( ! ($key->cacheMode & Cache::MODE_GET)) {
74
            return null;
75 81
        }
76
77 81
        $cacheEntry = $this->region->get($key);
78
79 81
        if ( ! $cacheEntry instanceof QueryCacheEntry) {
80 81
            return null;
81 81
        }
82 81
83 81
        if ( ! $this->validator->isValid($key, $cacheEntry)) {
84 81
            $this->region->evict($key);
85
86
            return null;
87
        }
88
89 47
        $result      = [];
90
        $entityName  = reset($rsm->aliasMap);
91 47
        $hasRelation = ( ! empty($rsm->relationMap));
92 3
        $unitOfWork  = $this->em->getUnitOfWork();
93
        $persister   = $unitOfWork->getEntityPersister($entityName);
94
        $region      = $persister->getCacheRegion();
95 46
        $regionName  = $region->getName();
96
97 46
        $cm = $this->em->getClassMetadata($entityName);
98 43
99
        $generateKeys = function (array $entry) use ($cm): EntityCacheKey {
100
            return new EntityCacheKey($cm->getRootClassName(), $entry['identifier']);
0 ignored issues
show
Bug introduced by
The method getRootClassName() does not seem to exist on object<Doctrine\Common\P...\Mapping\ClassMetadata>.

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...
101 33
        };
102 2
103
        $cacheKeys = new CollectionCacheEntry(array_map($generateKeys, $cacheEntry->result));
104 2
        $entries   = $region->getMultiple($cacheKeys);
105
106
        // @TODO - move to cache hydration component
107 31
        foreach ($cacheEntry->result as $index => $entry) {
108 31
            $entityEntry = is_array($entries) ? ($entries[$index] ?? null) : null;
109 31
110 31
            if ($entityEntry === null) {
111 31
                if ($this->cacheLogger !== null) {
112 31
                    $this->cacheLogger->entityCacheMiss($regionName, $cacheKeys->identifiers[$index]);
0 ignored issues
show
Compatibility introduced by
$cacheKeys->identifiers[$index] of type object<Doctrine\ORM\Cache\CacheKey> is not a sub-type of object<Doctrine\ORM\Cache\EntityCacheKey>. It seems like you assume a child class of the class Doctrine\ORM\Cache\CacheKey to be always present.

This check looks for parameters that are defined as one type in their type hint or doc comment but seem to be used as a narrower type, i.e an implementation of an interface or a subclass.

Consider changing the type of the parameter or doing an instanceof check before assuming your parameter is of the expected type.

Loading history...
113
                }
114 31
115
                return null;
116 31
            }
117
118 31
            if ($this->cacheLogger !== null) {
119
                $this->cacheLogger->entityCacheHit($regionName, $cacheKeys->identifiers[$index]);
0 ignored issues
show
Compatibility introduced by
$cacheKeys->identifiers[$index] of type object<Doctrine\ORM\Cache\CacheKey> is not a sub-type of object<Doctrine\ORM\Cache\EntityCacheKey>. It seems like you assume a child class of the class Doctrine\ORM\Cache\CacheKey to be always present.

This check looks for parameters that are defined as one type in their type hint or doc comment but seem to be used as a narrower type, i.e an implementation of an interface or a subclass.

Consider changing the type of the parameter or doing an instanceof check before assuming your parameter is of the expected type.

Loading history...
120 2
            }
121 1
122
            if ( ! $hasRelation) {
123
                $result[$index] = $unitOfWork->createEntity(
124 2
                    $entityEntry->class,
125
                    $entityEntry->resolveAssociationEntries($this->em),
126
                    self::$hints
127 29
                );
128 28
129
                continue;
130
            }
131 29
132
            $data = $entityEntry->data;
133 21
134
            foreach ($entry['associations'] as $name => $assoc) {
135 21
                $assocPersister  = $unitOfWork->getEntityPersister($assoc['targetEntity']);
136
                $assocRegion     = $assocPersister->getCacheRegion();
137
                $assocMetadata   = $this->em->getClassMetadata($assoc['targetEntity']);
138 8
139
                // *-to-one association
140 8
                if (isset($assoc['identifier'])) {
141
                    $assocKey = new EntityCacheKey($assocMetadata->getRootClassName(), $assoc['identifier']);
0 ignored issues
show
Bug introduced by
The method getRootClassName() does not seem to exist on object<Doctrine\Common\P...\Mapping\ClassMetadata>.

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...
142 8
143 8 View Code Duplication
                    if (($assocEntry = $assocRegion->get($assocKey)) === null) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
144
                        if ($this->cacheLogger !== null) {
145 8
                            $this->cacheLogger->entityCacheMiss($assocRegion->getName(), $assocKey);
146
                        }
147 4
148
                        $unitOfWork->hydrationComplete();
149 1
150 1
                        return null;
151
                    }
152
153 1
                    $data[$name] = $unitOfWork->createEntity(
154
                        $assocEntry->class,
155 1
                        $assocEntry->resolveAssociationEntries($this->em),
156
                        self::$hints
157
                    );
158 3
159
                    if ($this->cacheLogger !== null) {
160 3
                        $this->cacheLogger->entityCacheHit($assocRegion->getName(), $assocKey);
161 3
                    }
162
163
                    continue;
164 3
                }
165
166
                if ( ! isset($assoc['list']) || empty($assoc['list'])) {
167 4
                    continue;
168
                }
169
170
                $generateKeys = function ($id) use ($assocMetadata): EntityCacheKey {
171 4
                    return new EntityCacheKey($assocMetadata->getRootClassName(), $id);
0 ignored issues
show
Bug introduced by
The method getRootClassName() does not seem to exist on object<Doctrine\Common\P...\Mapping\ClassMetadata>.

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...
172 4
                };
173
174 4
                $assocKeys    = new CollectionCacheEntry(array_map($generateKeys, $assoc['list']));
175
                $assocEntries = $assocRegion->getMultiple($assocKeys);
176 4
177
                // *-to-many association
178 1
                $collection = [];
179 1
180
                foreach ($assoc['list'] as $assocIndex => $assocId) {
181
                    $assocEntry = is_array($assocEntries) ? ($assocEntries[$assocIndex] ?? null) : null;
182 1
183 View Code Duplication
                    if ($assocEntry === null) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
184 1
                        if ($this->cacheLogger !== null) {
185
                            $this->cacheLogger->entityCacheMiss($assocRegion->getName(), $assocKeys->identifiers[$assocIndex]);
0 ignored issues
show
Compatibility introduced by
$assocKeys->identifiers[$assocIndex] of type object<Doctrine\ORM\Cache\CacheKey> is not a sub-type of object<Doctrine\ORM\Cache\EntityCacheKey>. It seems like you assume a child class of the class Doctrine\ORM\Cache\CacheKey to be always present.

This check looks for parameters that are defined as one type in their type hint or doc comment but seem to be used as a narrower type, i.e an implementation of an interface or a subclass.

Consider changing the type of the parameter or doing an instanceof check before assuming your parameter is of the expected type.

Loading history...
186
                        }
187 3
188
                        $unitOfWork->hydrationComplete();
189 3
190
                        return null;
191 3
                    }
192 3
193
                    $collection[$assocIndex] = $unitOfWork->createEntity(
194
                        $assocEntry->class,
195
                        $assocEntry->resolveAssociationEntries($this->em),
196 3
                        self::$hints
197
                    );
198 3
199
                    if ($this->cacheLogger !== null) {
200
                        $this->cacheLogger->entityCacheHit($assocRegion->getName(), $assocKeys->identifiers[$assocIndex]);
0 ignored issues
show
Compatibility introduced by
$assocKeys->identifiers[$assocIndex] of type object<Doctrine\ORM\Cache\CacheKey> is not a sub-type of object<Doctrine\ORM\Cache\EntityCacheKey>. It seems like you assume a child class of the class Doctrine\ORM\Cache\CacheKey to be always present.

This check looks for parameters that are defined as one type in their type hint or doc comment but seem to be used as a narrower type, i.e an implementation of an interface or a subclass.

Consider changing the type of the parameter or doing an instanceof check before assuming your parameter is of the expected type.

Loading history...
201 6
                    }
202
                }
203
204 27
                $data[$name] = $collection;
205
            }
206 27
207
            foreach ($data as $fieldName => $unCachedAssociationData) {
208
                // In some scenarios, such as EAGER+ASSOCIATION+ID+CACHE, the
209
                // cache key information in `$cacheEntry` will not contain details
210
                // for fields that are associations.
211
                //
212 54
                // This means that `$data` keys for some associations that may
213
                // actually not be cached will not be converted to actual association
214 54
                // data, yet they contain L2 cache AssociationCacheEntry objects.
215 1
                //
216
                // We need to unwrap those associations into proxy references,
217
                // since we don't have actual data for them except for identifiers.
218 53
                if ($unCachedAssociationData instanceof AssociationCacheEntry) {
219 1
                    $data[$fieldName] = $this->em->getReference(
220
                        $unCachedAssociationData->class,
221
                        $unCachedAssociationData->identifier
222 52
                    );
223 2
                }
224
            }
225
226 50
            $result[$index] = $unitOfWork->createEntity($entityEntry->class, $data, self::$hints);
227 1
        }
228
229
        $unitOfWork->hydrationComplete();
230 49
231 3
        return $result;
232
    }
233
234 48
    /**
235 48
     * {@inheritdoc}
236 48
     */
237 48
    public function put(QueryCacheKey $key, ResultSetMapping $rsm, $result, array $hints = [])
238 48
    {
239
        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...
240 48
            throw new CacheException("Second level cache does not support scalar results.");
241 1
        }
242
243
        if (count($rsm->entityMappings) > 1) {
244 47
            throw new CacheException("Second level cache does not support multiple root entities.");
245
        }
246 47
247 47
        if ( ! $rsm->isSelect) {
248 47
            throw new CacheException("Second-level cache query supports only select statements.");
249 47
        }
250 47
251
        if (isset($hints[Query::HINT_FORCE_PARTIAL_LOAD]) && $hints[Query::HINT_FORCE_PARTIAL_LOAD]) {
252 47
            throw new CacheException("Second level cache does not support partial entities.");
253
        }
254 35
255 1
        if ( ! ($key->cacheMode & Cache::MODE_PUT)) {
256
            return false;
257
        }
258
259 46
        $data        = [];
260 32
        $entityName  = reset($rsm->aliasMap);
261
        $rootAlias   = key($rsm->aliasMap);
262
        $unitOfWork  = $this->em->getUnitOfWork();
263
        $persister   = $unitOfWork->getEntityPersister($entityName);
264 14
265 14
        if ( ! ($persister instanceof CachedPersister)) {
266 14
            throw CacheException::nonCacheableEntity($entityName);
267 14
        }
268 14
269 14
        $region = $persister->getCacheRegion();
270
271 14
        foreach ($result as $index => $entity) {
272 1
            $identifier                     = $unitOfWork->getEntityIdentifier($entity);
273
            $entityKey                      = new EntityCacheKey($entityName, $identifier);
274
275
            if (($key->cacheMode & Cache::MODE_REFRESH) || ! $region->contains($entityKey)) {
276 13
                // Cancel put result if entity put fail
277
                if ( ! $persister->storeEntityCache($entity, $entityKey)) {
278 13
                    return false;
279 2
                }
280
            }
281
282 11
            $data[$index]['identifier']   = $identifier;
283
            $data[$index]['associations'] = [];
284 11
285
            // @TODO - move to cache hydration components
286
            foreach ($rsm->relationMap as $alias => $name) {
287
                $parentAlias  = $rsm->parentAliasMap[$alias];
288 2
                $parentClass  = $rsm->aliasMap[$parentAlias];
289
                $metadata     = $this->em->getClassMetadata($parentClass);
290 1
                $association  = $metadata->getProperty($name);
0 ignored issues
show
Bug introduced by
The method getProperty() does not seem to exist on object<Doctrine\Common\P...\Mapping\ClassMetadata>.

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...
291
                $assocValue   = $this->getAssociationValue($rsm, $alias, $entity);
292
293
                if ($assocValue === null) {
294 1
                    continue;
295
                }
296
297
                // root entity association
298 1
                if ($rootAlias === $parentAlias) {
299
                    // Cancel put result if association put fail
300 1
                    if (($assocInfo = $this->storeAssociationCache($key, $association, $assocValue)) === null) {
301 12
                        return false;
302
                    }
303
304
                    $data[$index]['associations'][$name] = $assocInfo;
305
306
                    continue;
307 44
                }
308
309
                // store single nested association
310
                if ( ! is_array($assocValue)) {
311
                    // Cancel put result if association put fail
312
                    if ($this->storeAssociationCache($key, $association, $assocValue) === null) {
313
                        return false;
314
                    }
315
316
                    continue;
317 13
                }
318
319 13
                // store array of nested association
320 13
                foreach ($assocValue as $aVal) {
321 13
                    // Cancel put result if association put fail
322
                    if ($this->storeAssociationCache($key, $association, $aVal) === null) {
323
                        return false;
324 13
                    }
325 7
                }
326 7
            }
327
        }
328 7
329
        return $this->region->put($key, new QueryCacheEntry($data));
330 7
    }
331 1
332
    /**
333
     * @param \Doctrine\ORM\Cache\QueryCacheKey $key
334
     * @param AssociationMetadata               $assoc
0 ignored issues
show
Bug introduced by
There is no parameter named $assoc. Was it maybe removed?

This check looks for PHPDoc comments describing methods or function parameters that do not exist on the corresponding method or function.

Consider the following example. The parameter $italy is not defined by the method finale(...).

/**
 * @param array $germany
 * @param array $island
 * @param array $italy
 */
function finale($germany, $island) {
    return "2:1";
}

The most likely cause is that the parameter was removed, but the annotation was not.

Loading history...
335
     * @param mixed                             $assocValue
336 6
     *
337 6
     * @return array|null
338 6
     */
339
    private function storeAssociationCache(QueryCacheKey $key, AssociationMetadata $association, $assocValue)
340
    {
341
        $unitOfWork     = $this->em->getUnitOfWork();
342
        $assocPersister = $unitOfWork->getEntityPersister($association->getTargetEntity());
343 6
        $assocMetadata  = $assocPersister->getClassMetadata();
344
        $assocRegion    = $assocPersister->getCacheRegion();
345 6
346 6
        // Handle *-to-one associations
347 6
        if ($association instanceof ToOneAssociationMetadata) {
348
            $assocIdentifier = $unitOfWork->getEntityIdentifier($assocValue);
349 6
            $entityKey       = new EntityCacheKey($assocMetadata->getRootClassName(), $assocIdentifier);
350
351 6
            if ((! $assocValue instanceof GhostObjectInterface && ($key->cacheMode & Cache::MODE_REFRESH)) || ! $assocRegion->contains($entityKey)) {
352 1
                // Entity put fail
353
                if (! $assocPersister->storeEntityCache($assocValue, $entityKey)) {
354
                    return null;
355
                }
356 5
            }
357
358
            return [
359
                'targetEntity'  => $assocMetadata->getRootClassName(),
360 5
                'identifier'    => $assocIdentifier,
361 5
            ];
362 5
        }
363
364
        // Handle *-to-many associations
365
        $list = [];
366
367
        foreach ($assocValue as $assocItemIndex => $assocItem) {
368
            $assocIdentifier = $unitOfWork->getEntityIdentifier($assocItem);
369
            $entityKey       = new EntityCacheKey($assocMetadata->getRootClassName(), $assocIdentifier);
370
371
            if (($key->cacheMode & Cache::MODE_REFRESH) || ! $assocRegion->contains($entityKey)) {
372
                // Entity put fail
373 15
                if ( ! $assocPersister->storeEntityCache($assocItem, $entityKey)) {
374
                    return null;
375 15
                }
376 15
            }
377
378 15
            $list[$assocItemIndex] = $assocIdentifier;
379 15
        }
380 15
381 15
        return [
382
            'targetEntity' => $assocMetadata->getRootClassName(),
383 15
            'list'         => $list,
384 15
        ];
385 15
    }
386
387
    /**
388 15
     * @param \Doctrine\ORM\Query\ResultSetMapping $rsm
389
     * @param string                               $assocAlias
390
     * @param object                               $entity
391 15
     *
392
     * @return array|object
393
     */
394
    private function getAssociationValue(ResultSetMapping $rsm, $assocAlias, $entity)
395
    {
396
        $path  = [];
397
        $alias = $assocAlias;
398
399
        while (isset($rsm->parentAliasMap[$alias])) {
400 15
            $parent = $rsm->parentAliasMap[$alias];
401
            $field  = $rsm->relationMap[$alias];
402 15
            $class  = $rsm->aliasMap[$parent];
403 15
404 15
            array_unshift($path, [
405 15
                'field'  => $field,
406
                'class'  => $class
407 15
            ]
408 1
            );
409
410
            $alias = $parent;
411 14
        }
412 14
413
        return $this->getAssociationPathValue($entity, $path);
414
    }
415
416 3
    /**
417 1
     * @param mixed $value
418
     * @param array $path
419
     *
420 2
     * @return array|object|null
421
     */
422 2
    private function getAssociationPathValue($value, array $path)
423 2
    {
424
        $mapping     = array_shift($path);
425
        $metadata    = $this->em->getClassMetadata($mapping['class']);
426 2
        $association = $metadata->getProperty($mapping['field']);
0 ignored issues
show
Bug introduced by
The method getProperty() does not seem to exist on object<Doctrine\Common\P...\Mapping\ClassMetadata>.

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...
427
        $value       = $association->getValue($value);
428
429
        if ($value === null) {
430
            return null;
431
        }
432 47
433
        if (empty($path)) {
434 47
            return $value;
435
        }
436
437
        // Handle *-to-one associations
438
        if ($association instanceof ToOneAssociationMetadata) {
439
            return $this->getAssociationPathValue($value, $path);
440 25
        }
441
442 25
        $values = [];
443
444
        foreach ($value as $item) {
445
            $values[] = $this->getAssociationPathValue($item, $path);
446
        }
447
448
        return $values;
449
    }
450
451
    /**
452
     * {@inheritdoc}
453
     */
454
    public function clear()
455
    {
456
        return $this->region->evictAll();
457
    }
458
459
    /**
460
     * {@inheritdoc}
461
     */
462
    public function getRegion()
463
    {
464
        return $this->region;
465
    }
466
}
467