Failed Conditions
Pull Request — 2.6 (#7778)
by Luís
06:24
created

DefaultQueryCache   F

Complexity

Total Complexity 64

Size/Duplication

Total Lines 434
Duplicated Lines 0 %

Test Coverage

Coverage 98.93%

Importance

Changes 5
Bugs 0 Features 0
Metric Value
eloc 184
dl 0
loc 434
ccs 185
cts 187
cp 0.9893
rs 3.28
c 5
b 0
f 0
wmc 64

8 Methods

Rating   Name   Duplication   Size   Complexity  
A __construct() 0 9 1
B storeAssociationCache() 0 46 10
A clear() 0 3 1
A getAssociationValue() 0 20 2
A getRegion() 0 3 1
A getAssociationPathValue() 0 27 5
D get() 0 151 24
D put() 0 92 20

How to fix   Complexity   

Complex Class

Complex classes like DefaultQueryCache often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use DefaultQueryCache, and based on these observations, apply Extract Interface, too.

1
<?php
2
3
/*
4
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
5
 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
6
 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
7
 * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
8
 * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
9
 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
10
 * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
11
 * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
12
 * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
13
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
14
 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
15
 *
16
 * This software consists of voluntary contributions made by many individuals
17
 * and is licensed under the MIT license. For more information, see
18
 * <http://www.doctrine-project.org>.
19
 */
20
21
namespace Doctrine\ORM\Cache;
22
23
use Doctrine\Common\Collections\ArrayCollection;
24
use Doctrine\ORM\Cache\Persister\CachedPersister;
25
use Doctrine\ORM\Cache\Persister\Entity\CachedEntityPersister;
26
use Doctrine\ORM\EntityManagerInterface;
27
use Doctrine\ORM\Query\ResultSetMapping;
28
use Doctrine\ORM\Mapping\ClassMetadata;
29
use Doctrine\ORM\PersistentCollection;
30
use Doctrine\Common\Proxy\Proxy;
31
use Doctrine\ORM\Cache;
32
use Doctrine\ORM\Query;
33
use function assert;
34
35
/**
36
 * Default query cache implementation.
37
 *
38
 * @since   2.5
39
 * @author  Fabio B. Silva <[email protected]>
40
 */
41
class DefaultQueryCache implements QueryCache
42
{
43
     /**
44
     * @var \Doctrine\ORM\EntityManagerInterface
45
     */
46
    private $em;
47
48
    /**
49
     * @var \Doctrine\ORM\UnitOfWork
50
     */
51
    private $uow;
52
53
    /**
54
     * @var \Doctrine\ORM\Cache\Region
55
     */
56
    private $region;
57
58
    /**
59
     * @var \Doctrine\ORM\Cache\QueryCacheValidator
60
     */
61
    private $validator;
62
63
    /**
64
     * @var \Doctrine\ORM\Cache\Logging\CacheLogger
65
     */
66
    protected $cacheLogger;
67
68
    /**
69
     * @var array
70
     */
71
    private static $hints = [Query::HINT_CACHE_ENABLED => true];
72
73
    /**
74
     * @param \Doctrine\ORM\EntityManagerInterface $em     The entity manager.
75
     * @param \Doctrine\ORM\Cache\Region           $region The query region.
76
     */
77 87
    public function __construct(EntityManagerInterface $em, Region $region)
78
    {
79 87
        $cacheConfig = $em->getConfiguration()->getSecondLevelCacheConfiguration();
80
81 87
        $this->em           = $em;
82 87
        $this->region       = $region;
83 87
        $this->uow          = $em->getUnitOfWork();
84 87
        $this->cacheLogger  = $cacheConfig->getCacheLogger();
85 87
        $this->validator    = $cacheConfig->getQueryValidator();
86 87
    }
87
88
    /**
89
     * {@inheritdoc}
90
     */
91 53
    public function get(QueryCacheKey $key, ResultSetMapping $rsm, array $hints = [])
92
    {
93 53
        if ( ! ($key->cacheMode & Cache::MODE_GET)) {
94 3
            return null;
95
        }
96
97 52
        $cacheEntry = $this->region->get($key);
98
99 52
        if ( ! $cacheEntry instanceof QueryCacheEntry) {
100 47
            return null;
101
        }
102
103 39
        if ( ! $this->validator->isValid($key, $cacheEntry)) {
104 8
            $this->region->evict($key);
105
106 8
            return null;
107
        }
108
109 34
        $result      = [];
110 34
        $entityName  = reset($rsm->aliasMap);
111 34
        $hasRelation = ! empty($rsm->relationMap);
112 34
        $persister   = $this->uow->getEntityPersister($entityName);
113 34
        assert($persister instanceof CachedEntityPersister);
114
115 34
        $region     = $persister->getCacheRegion();
116 34
        $regionName = $region->getName();
117
118 34
        $cm = $this->em->getClassMetadata($entityName);
119
120 34
        $generateKeys = static function (array $entry) use ($cm): EntityCacheKey {
121 34
            return new EntityCacheKey($cm->rootEntityName, $entry['identifier']);
0 ignored issues
show
Bug introduced by
Accessing rootEntityName on the interface Doctrine\Common\Persistence\Mapping\ClassMetadata suggest that you code against a concrete implementation. How about adding an instanceof check?
Loading history...
122 34
        };
123
124 34
        $cacheKeys = new CollectionCacheEntry(array_map($generateKeys, $cacheEntry->result));
125 34
        $entries   = $region->getMultiple($cacheKeys) ?? [];
126
127
        // @TODO - move to cache hydration component
128 34
        foreach ($cacheEntry->result as $index => $entry) {
129 34
            $entityEntry = $entries[$index] ?? null;
130
131 34
            if (! $entityEntry instanceof EntityCacheEntry) {
132 3
                if ($this->cacheLogger !== null) {
133 1
                    $this->cacheLogger->entityCacheMiss($regionName, $cacheKeys->identifiers[$index]);
134
                }
135
136 3
                return null;
137
            }
138
139 32
            if ($this->cacheLogger !== null) {
140 29
                $this->cacheLogger->entityCacheHit($regionName, $cacheKeys->identifiers[$index]);
141
            }
142
143 32
            if ( ! $hasRelation) {
144 23
                $result[$index]  = $this->uow->createEntity($entityEntry->class, $entityEntry->resolveAssociationEntries($this->em), self::$hints);
145
146 23
                continue;
147
            }
148
149 9
            $data = $entityEntry->data;
150
151 9
            foreach ($entry['associations'] as $name => $assoc) {
152 9
                $assocPersister = $this->uow->getEntityPersister($assoc['targetEntity']);
153 9
                $assocRegion    = $assocPersister->getCacheRegion();
0 ignored issues
show
Bug introduced by
The method getCacheRegion() does not exist on Doctrine\ORM\Persisters\Entity\EntityPersister. It seems like you code against a sub-type of Doctrine\ORM\Persisters\Entity\EntityPersister such as Doctrine\ORM\Cache\Persi...y\CachedEntityPersister. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

153
                /** @scrutinizer ignore-call */ 
154
                $assocRegion    = $assocPersister->getCacheRegion();
Loading history...
154 9
                $assocMetadata  = $this->em->getClassMetadata($assoc['targetEntity']);
155
156 9
                if ($assoc['type'] & ClassMetadata::TO_ONE) {
157
158 5
                    if (($assocEntry = $assocRegion->get($assocKey = new EntityCacheKey($assocMetadata->rootEntityName, $assoc['identifier']))) === null) {
0 ignored issues
show
Bug introduced by
Accessing rootEntityName on the interface Doctrine\Common\Persistence\Mapping\ClassMetadata suggest that you code against a concrete implementation. How about adding an instanceof check?
Loading history...
159
160 1
                        if ($this->cacheLogger !== null) {
161 1
                            $this->cacheLogger->entityCacheMiss($assocRegion->getName(), $assocKey);
162
                        }
163
164 1
                        $this->uow->hydrationComplete();
165
166 1
                        return null;
167
                    }
168
169 4
                    $data[$name] = $this->uow->createEntity($assocEntry->class, $assocEntry->resolveAssociationEntries($this->em), self::$hints);
170
171 4
                    if ($this->cacheLogger !== null) {
172 4
                        $this->cacheLogger->entityCacheHit($assocRegion->getName(), $assocKey);
173
                    }
174
175 4
                    continue;
176
                }
177
178 4
                if ( ! isset($assoc['list']) || empty($assoc['list'])) {
179
                    continue;
180
                }
181
182 4
                $generateKeys = function ($id) use ($assocMetadata): EntityCacheKey {
183 4
                    return new EntityCacheKey($assocMetadata->rootEntityName, $id);
0 ignored issues
show
Bug introduced by
Accessing rootEntityName on the interface Doctrine\Common\Persistence\Mapping\ClassMetadata suggest that you code against a concrete implementation. How about adding an instanceof check?
Loading history...
184 4
                };
185
186 4
                $collection   = new PersistentCollection($this->em, $assocMetadata, new ArrayCollection());
187 4
                $assocKeys    = new CollectionCacheEntry(array_map($generateKeys, $assoc['list']));
188 4
                $assocEntries = $assocRegion->getMultiple($assocKeys);
189
190 4
                foreach ($assoc['list'] as $assocIndex => $assocId) {
191 4
                    $assocEntry = is_array($assocEntries) && array_key_exists($assocIndex, $assocEntries) ? $assocEntries[$assocIndex] : null;
192
193 4
                    if ($assocEntry === null) {
194 1
                        if ($this->cacheLogger !== null) {
195 1
                            $this->cacheLogger->entityCacheMiss($assocRegion->getName(), $assocKeys->identifiers[$assocIndex]);
196
                        }
197
198 1
                        $this->uow->hydrationComplete();
199
200 1
                        return null;
201
                    }
202
203 3
                    $element = $this->uow->createEntity($assocEntry->class, $assocEntry->resolveAssociationEntries($this->em), self::$hints);
204
205 3
                    $collection->hydrateSet($assocIndex, $element);
206
207 3
                    if ($this->cacheLogger !== null) {
208 3
                        $this->cacheLogger->entityCacheHit($assocRegion->getName(), $assocKeys->identifiers[$assocIndex]);
209
                    }
210
                }
211
212 3
                $data[$name] = $collection;
213
214 3
                $collection->setInitialized(true);
215
            }
216
217 7
            foreach ($data as $fieldName => $unCachedAssociationData) {
218
                // In some scenarios, such as EAGER+ASSOCIATION+ID+CACHE, the
219
                // cache key information in `$cacheEntry` will not contain details
220
                // for fields that are associations.
221
                //
222
                // This means that `$data` keys for some associations that may
223
                // actually not be cached will not be converted to actual association
224
                // data, yet they contain L2 cache AssociationCacheEntry objects.
225
                //
226
                // We need to unwrap those associations into proxy references,
227
                // since we don't have actual data for them except for identifiers.
228 7
                if ($unCachedAssociationData instanceof AssociationCacheEntry) {
229 4
                    $data[$fieldName] = $this->em->getReference(
230 4
                        $unCachedAssociationData->class,
231 7
                        $unCachedAssociationData->identifier
232
                    );
233
                }
234
            }
235
236 7
            $result[$index] = $this->uow->createEntity($entityEntry->class, $data, self::$hints);
237
        }
238
239 29
        $this->uow->hydrationComplete();
240
241 29
        return $result;
242
    }
243
244
    /**
245
     * {@inheritdoc}
246
     */
247 58
    public function put(QueryCacheKey $key, ResultSetMapping $rsm, $result, array $hints = [])
248
    {
249 58
        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...
250 1
            throw new CacheException("Second level cache does not support scalar results.");
251
        }
252
253 57
        if (count($rsm->entityMappings) > 1) {
254 1
            throw new CacheException("Second level cache does not support multiple root entities.");
255
        }
256
257 56
        if ( ! $rsm->isSelect) {
258 2
            throw new CacheException("Second-level cache query supports only select statements.");
259
        }
260
261 54
        if (isset($hints[Query::HINT_FORCE_PARTIAL_LOAD]) && $hints[Query::HINT_FORCE_PARTIAL_LOAD]) {
262 1
            throw new CacheException("Second level cache does not support partial entities.");
263
        }
264
265 53
        if ( ! ($key->cacheMode & Cache::MODE_PUT)) {
266 3
            return false;
267
        }
268
269 52
        $data        = [];
270 52
        $entityName  = reset($rsm->aliasMap);
271 52
        $rootAlias   = key($rsm->aliasMap);
272 52
        $persister   = $this->uow->getEntityPersister($entityName);
273
274 52
        if (! $persister instanceof CachedEntityPersister) {
275 1
            throw CacheException::nonCacheableEntity($entityName);
276
        }
277
278 51
        $region = $persister->getCacheRegion();
279
280 51
        foreach ($result as $index => $entity) {
281 51
            $identifier                     = $this->uow->getEntityIdentifier($entity);
282 51
            $entityKey                      = new EntityCacheKey($entityName, $identifier);
283
284 51
            if (($key->cacheMode & Cache::MODE_REFRESH) || ! $region->contains($entityKey)) {
285
                // Cancel put result if entity put fail
286 36
                if ( ! $persister->storeEntityCache($entity, $entityKey)) {
287 1
                    return false;
288
                }
289
            }
290
291 50
            $data[$index]['identifier']   = $identifier;
292 50
            $data[$index]['associations'] = [];
293
294
            // @TODO - move to cache hydration components
295 50
            foreach ($rsm->relationMap as $alias => $name) {
296 15
                $parentAlias  = $rsm->parentAliasMap[$alias];
297 15
                $parentClass  = $rsm->aliasMap[$parentAlias];
298 15
                $metadata     = $this->em->getClassMetadata($parentClass);
299 15
                $assoc        = $metadata->associationMappings[$name];
0 ignored issues
show
Bug introduced by
Accessing associationMappings on the interface Doctrine\Common\Persistence\Mapping\ClassMetadata suggest that you code against a concrete implementation. How about adding an instanceof check?
Loading history...
300 15
                $assocValue   = $this->getAssociationValue($rsm, $alias, $entity);
301
302 15
                if ($assocValue === null) {
303 1
                    continue;
304
                }
305
306
                // root entity association
307 14
                if ($rootAlias === $parentAlias) {
308
                    // Cancel put result if association put fail
309 14
                    if ( ($assocInfo = $this->storeAssociationCache($key, $assoc, $assocValue)) === null) {
310 2
                        return false;
311
                    }
312
313 12
                    $data[$index]['associations'][$name] = $assocInfo;
314
315 12
                    continue;
316
                }
317
318
                // store single nested association
319 2
                if ( ! is_array($assocValue)) {
320
                    // Cancel put result if association put fail
321 1
                    if ($this->storeAssociationCache($key, $assoc, $assocValue) === null) {
322
                        return false;
323
                    }
324
325 1
                    continue;
326
                }
327
328
                // store array of nested association
329 1
                foreach ($assocValue as $aVal) {
330
                    // Cancel put result if association put fail
331 1
                    if ($this->storeAssociationCache($key, $assoc, $aVal) === null) {
332 48
                        return false;
333
                    }
334
                }
335
            }
336
        }
337
338 48
        return $this->region->put($key, new QueryCacheEntry($data));
339
    }
340
341
    /**
342
     * @param \Doctrine\ORM\Cache\QueryCacheKey $key
343
     * @param array                             $assoc
344
     * @param mixed                             $assocValue
345
     *
346
     * @return array|null
347
     */
348 14
    private function storeAssociationCache(QueryCacheKey $key, array $assoc, $assocValue)
349
    {
350 14
        $assocPersister = $this->uow->getEntityPersister($assoc['targetEntity']);
351 14
        $assocMetadata  = $assocPersister->getClassMetadata();
352 14
        $assocRegion    = $assocPersister->getCacheRegion();
353
354
        // Handle *-to-one associations
355 14
        if ($assoc['type'] & ClassMetadata::TO_ONE) {
356 8
            $assocIdentifier = $this->uow->getEntityIdentifier($assocValue);
357 8
            $entityKey       = new EntityCacheKey($assocMetadata->rootEntityName, $assocIdentifier);
358
359 8
            if ( ! $assocValue instanceof Proxy && ($key->cacheMode & Cache::MODE_REFRESH) || ! $assocRegion->contains($entityKey)) {
0 ignored issues
show
introduced by
Consider adding parentheses for clarity. Current Interpretation: (! $assocValue instanceo...n->contains($entityKey), Probably Intended Meaning: ! $assocValue instanceof...->contains($entityKey))
Loading history...
360
                // Entity put fail
361 7
                if ( ! $assocPersister->storeEntityCache($assocValue, $entityKey)) {
0 ignored issues
show
Bug introduced by
The method storeEntityCache() does not exist on Doctrine\ORM\Persisters\Entity\EntityPersister. It seems like you code against a sub-type of Doctrine\ORM\Persisters\Entity\EntityPersister such as Doctrine\ORM\Cache\Persi...y\CachedEntityPersister. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

361
                if ( ! $assocPersister->/** @scrutinizer ignore-call */ storeEntityCache($assocValue, $entityKey)) {
Loading history...
362 1
                    return null;
363
                }
364
            }
365
366
            return [
367 7
                'targetEntity'  => $assocMetadata->rootEntityName,
368 7
                'identifier'    => $assocIdentifier,
369 7
                'type'          => $assoc['type']
370
            ];
371
        }
372
373
        // Handle *-to-many associations
374 6
        $list = [];
375
376 6
        foreach ($assocValue as $assocItemIndex => $assocItem) {
377 6
            $assocIdentifier = $this->uow->getEntityIdentifier($assocItem);
378 6
            $entityKey       = new EntityCacheKey($assocMetadata->rootEntityName, $assocIdentifier);
379
380 6
            if (($key->cacheMode & Cache::MODE_REFRESH) || ! $assocRegion->contains($entityKey)) {
381
                // Entity put fail
382 6
                if ( ! $assocPersister->storeEntityCache($assocItem, $entityKey)) {
383 1
                    return null;
384
                }
385
            }
386
387 5
            $list[$assocItemIndex] = $assocIdentifier;
388
        }
389
390
        return [
391 5
            'targetEntity'  => $assocMetadata->rootEntityName,
392 5
            'type'          => $assoc['type'],
393 5
            'list'          => $list,
394
        ];
395
    }
396
397
    /**
398
     * @param \Doctrine\ORM\Query\ResultSetMapping $rsm
399
     * @param string                               $assocAlias
400
     * @param object                               $entity
401
     *
402
     * @return array|object
403
     */
404 16
    private function getAssociationValue(ResultSetMapping $rsm, $assocAlias, $entity)
405
    {
406 16
        $path  = [];
407 16
        $alias = $assocAlias;
408
409 16
        while (isset($rsm->parentAliasMap[$alias])) {
410 16
            $parent = $rsm->parentAliasMap[$alias];
411 16
            $field  = $rsm->relationMap[$alias];
412 16
            $class  = $rsm->aliasMap[$parent];
413
414 16
            array_unshift($path, [
415 16
                'field'  => $field,
416 16
                'class'  => $class
417
            ]
418
            );
419
420 16
            $alias = $parent;
421
        }
422
423 16
        return $this->getAssociationPathValue($entity, $path);
424
    }
425
426
    /**
427
     * @param mixed $value
428
     * @param array $path
429
     *
430
     * @return array|object|null
431
     */
432 16
    private function getAssociationPathValue($value, array $path)
433
    {
434 16
        $mapping  = array_shift($path);
435 16
        $metadata = $this->em->getClassMetadata($mapping['class']);
436 16
        $assoc    = $metadata->associationMappings[$mapping['field']];
0 ignored issues
show
Bug introduced by
Accessing associationMappings on the interface Doctrine\Common\Persistence\Mapping\ClassMetadata suggest that you code against a concrete implementation. How about adding an instanceof check?
Loading history...
437 16
        $value    = $metadata->getFieldValue($value, $mapping['field']);
0 ignored issues
show
Bug introduced by
The method getFieldValue() does not exist on Doctrine\Common\Persistence\Mapping\ClassMetadata. Did you maybe mean getFieldNames()? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

437
        /** @scrutinizer ignore-call */ 
438
        $value    = $metadata->getFieldValue($value, $mapping['field']);

This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.

This is most likely a typographical error or the method has been renamed.

Loading history...
438
439 16
        if ($value === null) {
440 1
            return null;
441
        }
442
443 15
        if (empty($path)) {
444 15
            return $value;
445
        }
446
447
        // Handle *-to-one associations
448 3
        if ($assoc['type'] & ClassMetadata::TO_ONE) {
449 1
            return $this->getAssociationPathValue($value, $path);
450
        }
451
452 2
        $values = [];
453
454 2
        foreach ($value as $item) {
455 2
            $values[] = $this->getAssociationPathValue($item, $path);
456
        }
457
458 2
        return $values;
459
    }
460
461
    /**
462
     * {@inheritdoc}
463
     */
464 48
    public function clear()
465
    {
466 48
        return $this->region->evictAll();
467
    }
468
469
    /**
470
     * {@inheritdoc}
471
     */
472 28
    public function getRegion()
473
    {
474 28
        return $this->region;
475
    }
476
}
477