Completed
Push — master ( 247b08...4f28aa )
by Luís
10:18
created

DefaultQueryCache   F

Complexity

Total Complexity 63

Size/Duplication

Total Lines 417
Duplicated Lines 0 %

Coupling/Cohesion

Components 1
Dependencies 16

Test Coverage

Coverage 98.88%

Importance

Changes 0
Metric Value
wmc 63
lcom 1
cbo 16
dl 0
loc 417
ccs 176
cts 178
cp 0.9888
rs 3.6585
c 0
b 0
f 0

8 Methods

Rating   Name   Duplication   Size   Complexity  
A __construct() 0 10 1
D get() 0 129 22
D put() 0 97 21
C storeAssociationCache() 0 48 10
A getAssociationValue() 0 21 2
B getAssociationPathValue() 0 28 5
A clear() 0 4 1
A getRegion() 0 4 1

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. You can also have a look at the cohesion graph to spot any un-connected, or weakly-connected components.

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\EntityManagerInterface;
26
use Doctrine\ORM\Query\ResultSetMapping;
27
use Doctrine\ORM\Mapping\ClassMetadata;
28
use Doctrine\ORM\PersistentCollection;
29
use Doctrine\Common\Proxy\Proxy;
30
use Doctrine\ORM\Cache;
31
use Doctrine\ORM\Query;
32
33
/**
34
 * Default query cache implementation.
35
 *
36
 * @since   2.5
37
 * @author  Fabio B. Silva <[email protected]>
38
 */
39
class DefaultQueryCache implements QueryCache
40
{
41
     /**
42
     * @var \Doctrine\ORM\EntityManagerInterface
43
     */
44
    private $em;
45
46
    /**
47
     * @var \Doctrine\ORM\UnitOfWork
48
     */
49
    private $uow;
50
51
    /**
52
     * @var \Doctrine\ORM\Cache\Region
53
     */
54
    private $region;
55
56
    /**
57
     * @var \Doctrine\ORM\Cache\QueryCacheValidator
58
     */
59
    private $validator;
60
61
    /**
62
     * @var \Doctrine\ORM\Cache\Logging\CacheLogger
63
     */
64
    protected $cacheLogger;
65
66
    /**
67
     * @var array
68
     */
69
    private static $hints = [Query::HINT_CACHE_ENABLED => true];
70
71
    /**
72
     * @param \Doctrine\ORM\EntityManagerInterface $em     The entity manager.
73
     * @param \Doctrine\ORM\Cache\Region           $region The query region.
74
     */
75 84
    public function __construct(EntityManagerInterface $em, Region $region)
76
    {
77 84
        $cacheConfig = $em->getConfiguration()->getSecondLevelCacheConfiguration();
78
79 84
        $this->em           = $em;
80 84
        $this->region       = $region;
81 84
        $this->uow          = $em->getUnitOfWork();
82 84
        $this->cacheLogger  = $cacheConfig->getCacheLogger();
83 84
        $this->validator    = $cacheConfig->getQueryValidator();
84 84
    }
85
86
    /**
87
     * {@inheritdoc}
88
     */
89 50
    public function get(QueryCacheKey $key, ResultSetMapping $rsm, array $hints = [])
90
    {
91 50
        if ( ! ($key->cacheMode & Cache::MODE_GET)) {
92 3
            return null;
93
        }
94
95 49
        $entry = $this->region->get($key);
96
97 49
        if ( ! $entry instanceof QueryCacheEntry) {
98 46
            return null;
99
        }
100
101 36
        if ( ! $this->validator->isValid($key, $entry)) {
102 8
            $this->region->evict($key);
103
104 8
            return null;
105
        }
106
107 31
        $result      = [];
108 31
        $entityName  = reset($rsm->aliasMap);
109 31
        $hasRelation = ( ! empty($rsm->relationMap));
110 31
        $persister   = $this->uow->getEntityPersister($entityName);
111 31
        $region      = $persister->getCacheRegion();
112 31
        $regionName  = $region->getName();
113
114 31
        $cm = $this->em->getClassMetadata($entityName);
115
116 31
        $generateKeys = function (array $entry) use ($cm): EntityCacheKey {
117 31
            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?

If you access a property on an interface, you most likely code against a concrete implementation of the interface.

Available Fixes

  1. Adding an additional type check:

    interface SomeInterface { }
    class SomeClass implements SomeInterface {
        public $a;
    }
    
    function someFunction(SomeInterface $object) {
        if ($object instanceof SomeClass) {
            $a = $object->a;
        }
    }
    
  2. Changing the type hint:

    interface SomeInterface { }
    class SomeClass implements SomeInterface {
        public $a;
    }
    
    function someFunction(SomeClass $object) {
        $a = $object->a;
    }
    
Loading history...
118 31
        };
119
120 31
        $cacheKeys = new CollectionCacheEntry(array_map($generateKeys, $entry->result));
121 31
        $entries   = $region->getMultiple($cacheKeys);
122
123
        // @TODO - move to cache hydration component
124 31
        foreach ($entry->result as $index => $entry) {
125
126 31
            $entityEntry = is_array($entries) && array_key_exists($index, $entries) ? $entries[$index] : null;
127
128 31
            if ($entityEntry === null) {
129
130 2
                if ($this->cacheLogger !== null) {
131 1
                    $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...
132
                }
133
134 2
                return null;
135
            }
136
137 29
            if ($this->cacheLogger !== null) {
138 28
                $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...
139
            }
140
141 29
            if ( ! $hasRelation) {
142
143 21
                $result[$index]  = $this->uow->createEntity($entityEntry->class, $entityEntry->resolveAssociationEntries($this->em), self::$hints);
144
145 21
                continue;
146
            }
147
148 8
            $data = $entityEntry->data;
149
150 8
            foreach ($entry['associations'] as $name => $assoc) {
151
152 8
                $assocPersister  = $this->uow->getEntityPersister($assoc['targetEntity']);
153 8
                $assocRegion     = $assocPersister->getCacheRegion();
154
155 8
                if ($assoc['type'] & ClassMetadata::TO_ONE) {
156
157 4
                    if (($assocEntry = $assocRegion->get($assocKey = new EntityCacheKey($assoc['targetEntity'], $assoc['identifier']))) === null) {
158
159 1
                        if ($this->cacheLogger !== null) {
160 1
                            $this->cacheLogger->entityCacheMiss($assocRegion->getName(), $assocKey);
161
                        }
162
163 1
                        $this->uow->hydrationComplete();
164
165 1
                        return null;
166
                    }
167
168 3
                    $data[$name] = $this->uow->createEntity($assocEntry->class, $assocEntry->resolveAssociationEntries($this->em), self::$hints);
169
170 3
                    if ($this->cacheLogger !== null) {
171 3
                        $this->cacheLogger->entityCacheHit($assocRegion->getName(), $assocKey);
172
                    }
173
174 3
                    continue;
175
                }
176
177 4
                if ( ! isset($assoc['list']) || empty($assoc['list'])) {
178
                    continue;
179
                }
180
181 4
                $targetClass = $this->em->getClassMetadata($assoc['targetEntity']);
182 4
                $collection  = new PersistentCollection($this->em, $targetClass, new ArrayCollection());
0 ignored issues
show
Compatibility introduced by
$targetClass of type object<Doctrine\Common\P...\Mapping\ClassMetadata> is not a sub-type of object<Doctrine\ORM\Mapping\ClassMetadata>. It seems like you assume a concrete implementation of the interface Doctrine\Common\Persistence\Mapping\ClassMetadata 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...
183
184 4
                foreach ($assoc['list'] as $assocIndex => $assocId) {
185
186 4
                    if (($assocEntry = $assocRegion->get($assocKey = new EntityCacheKey($assoc['targetEntity'], $assocId))) === null) {
187
188 1
                        if ($this->cacheLogger !== null) {
189 1
                            $this->cacheLogger->entityCacheMiss($assocRegion->getName(), $assocKey);
190
                        }
191
192 1
                        $this->uow->hydrationComplete();
193
194 1
                        return null;
195
                    }
196
197 3
                    $element = $this->uow->createEntity($assocEntry->class, $assocEntry->resolveAssociationEntries($this->em), self::$hints);
198
199 3
                    $collection->hydrateSet($assocIndex, $element);
200
201 3
                    if ($this->cacheLogger !== null) {
202 3
                        $this->cacheLogger->entityCacheHit($assocRegion->getName(), $assocKey);
203
                    }
204
                }
205
206 3
                $data[$name] = $collection;
207
208 3
                $collection->setInitialized(true);
209
            }
210
211 6
            $result[$index] = $this->uow->createEntity($entityEntry->class, $data, self::$hints);
212
        }
213
214 27
        $this->uow->hydrationComplete();
215
216 27
        return $result;
217
    }
218
219
    /**
220
     * {@inheritdoc}
221
     */
222 57
    public function put(QueryCacheKey $key, ResultSetMapping $rsm, $result, array $hints = [])
223
    {
224 57
        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...
225 1
            throw new CacheException("Second level cache does not support scalar results.");
226
        }
227
228 56
        if (count($rsm->entityMappings) > 1) {
229 1
            throw new CacheException("Second level cache does not support multiple root entities.");
230
        }
231
232 55
        if ( ! $rsm->isSelect) {
233 2
            throw new CacheException("Second-level cache query supports only select statements.");
234
        }
235
236 53
        if (isset($hints[Query::HINT_FORCE_PARTIAL_LOAD]) && $hints[Query::HINT_FORCE_PARTIAL_LOAD]) {
237 1
            throw new CacheException("Second level cache does not support partial entities.");
238
        }
239
240 52
        if ( ! ($key->cacheMode & Cache::MODE_PUT)) {
241 3
            return false;
242
        }
243
244 51
        $data        = [];
245 51
        $entityName  = reset($rsm->aliasMap);
246 51
        $rootAlias   = key($rsm->aliasMap);
247 51
        $hasRelation = ( ! empty($rsm->relationMap));
248 51
        $persister   = $this->uow->getEntityPersister($entityName);
249
250 51
        if ( ! ($persister instanceof CachedPersister)) {
251 1
            throw CacheException::nonCacheableEntity($entityName);
252
        }
253
254 50
        $region = $persister->getCacheRegion();
255
256 50
        foreach ($result as $index => $entity) {
257 50
            $identifier                     = $this->uow->getEntityIdentifier($entity);
258 50
            $entityKey                      = new EntityCacheKey($entityName, $identifier);
259 50
            $data[$index]['identifier']     = $identifier;
260 50
            $data[$index]['associations']   = [];
261
262 50
            if (($key->cacheMode & Cache::MODE_REFRESH) || ! $region->contains($entityKey)) {
263
                // Cancel put result if entity put fail
264 36
                if ( ! $persister->storeEntityCache($entity, $entityKey)) {
265 1
                    return false;
266
                }
267
            }
268
269 49
            if ( ! $hasRelation) {
270 35
                continue;
271
            }
272
273
            // @TODO - move to cache hydration components
274 14
            foreach ($rsm->relationMap as $alias => $name) {
275 14
                $parentAlias  = $rsm->parentAliasMap[$alias];
276 14
                $parentClass  = $rsm->aliasMap[$parentAlias];
277 14
                $metadata     = $this->em->getClassMetadata($parentClass);
278 14
                $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?

If you access a property on an interface, you most likely code against a concrete implementation of the interface.

Available Fixes

  1. Adding an additional type check:

    interface SomeInterface { }
    class SomeClass implements SomeInterface {
        public $a;
    }
    
    function someFunction(SomeInterface $object) {
        if ($object instanceof SomeClass) {
            $a = $object->a;
        }
    }
    
  2. Changing the type hint:

    interface SomeInterface { }
    class SomeClass implements SomeInterface {
        public $a;
    }
    
    function someFunction(SomeClass $object) {
        $a = $object->a;
    }
    
Loading history...
279 14
                $assocValue   = $this->getAssociationValue($rsm, $alias, $entity);
280
281 14
                if ($assocValue === null) {
282 1
                    continue;
283
                }
284
285
                // root entity association
286 13
                if ($rootAlias === $parentAlias) {
287
                    // Cancel put result if association put fail
288 13
                    if ( ($assocInfo = $this->storeAssociationCache($key, $assoc, $assocValue)) === null) {
289 2
                        return false;
290
                    }
291
292 11
                    $data[$index]['associations'][$name] = $assocInfo;
293
294 11
                    continue;
295
                }
296
297
                // store single nested association
298 2
                if ( ! is_array($assocValue)) {
299
                    // Cancel put result if association put fail
300 1
                    if ($this->storeAssociationCache($key, $assoc, $assocValue) === null) {
301
                        return false;
302
                    }
303
304 1
                    continue;
305
                }
306
307
                // store array of nested association
308 1
                foreach ($assocValue as $aVal) {
309
                    // Cancel put result if association put fail
310 1
                    if ($this->storeAssociationCache($key, $assoc, $aVal) === null) {
311 12
                        return false;
312
                    }
313
                }
314
            }
315
        }
316
317 47
        return $this->region->put($key, new QueryCacheEntry($data));
318
    }
319
320
    /**
321
     * @param \Doctrine\ORM\Cache\QueryCacheKey $key
322
     * @param array                             $assoc
323
     * @param mixed                             $assocValue
324
     *
325
     * @return array|null
326
     */
327 13
    private function storeAssociationCache(QueryCacheKey $key, array $assoc, $assocValue)
328
    {
329 13
        $assocPersister = $this->uow->getEntityPersister($assoc['targetEntity']);
330 13
        $assocMetadata  = $assocPersister->getClassMetadata();
331 13
        $assocRegion    = $assocPersister->getCacheRegion();
332
333
        // Handle *-to-one associations
334 13
        if ($assoc['type'] & ClassMetadata::TO_ONE) {
335 7
            $assocIdentifier = $this->uow->getEntityIdentifier($assocValue);
336 7
            $entityKey       = new EntityCacheKey($assocMetadata->rootEntityName, $assocIdentifier);
337
338 7
            if ( ! $assocValue instanceof Proxy && ($key->cacheMode & Cache::MODE_REFRESH) || ! $assocRegion->contains($entityKey)) {
339
                // Entity put fail
340 7
                if ( ! $assocPersister->storeEntityCache($assocValue, $entityKey)) {
341 1
                    return null;
342
                }
343
            }
344
345
            return [
346 6
                'targetEntity'  => $assocMetadata->rootEntityName,
347 6
                'identifier'    => $assocIdentifier,
348 6
                'type'          => $assoc['type']
349
            ];
350
        }
351
352
        // Handle *-to-many associations
353 6
        $list = [];
354
355 6
        foreach ($assocValue as $assocItemIndex => $assocItem) {
356 6
            $assocIdentifier = $this->uow->getEntityIdentifier($assocItem);
357 6
            $entityKey       = new EntityCacheKey($assocMetadata->rootEntityName, $assocIdentifier);
358
359 6
            if (($key->cacheMode & Cache::MODE_REFRESH) || ! $assocRegion->contains($entityKey)) {
360
                // Entity put fail
361 6
                if ( ! $assocPersister->storeEntityCache($assocItem, $entityKey)) {
362 1
                    return null;
363
                }
364
            }
365
366 5
            $list[$assocItemIndex] = $assocIdentifier;
367
        }
368
369
        return [
370 5
            'targetEntity'  => $assocMetadata->rootEntityName,
371 5
            'type'          => $assoc['type'],
372 5
            'list'          => $list,
373
        ];
374
    }
375
376
    /**
377
     * @param \Doctrine\ORM\Query\ResultSetMapping $rsm
378
     * @param string                               $assocAlias
379
     * @param object                               $entity
380
     *
381
     * @return array|object
382
     */
383 15
    private function getAssociationValue(ResultSetMapping $rsm, $assocAlias, $entity)
384
    {
385 15
        $path  = [];
386 15
        $alias = $assocAlias;
387
388 15
        while (isset($rsm->parentAliasMap[$alias])) {
389 15
            $parent = $rsm->parentAliasMap[$alias];
390 15
            $field  = $rsm->relationMap[$alias];
391 15
            $class  = $rsm->aliasMap[$parent];
392
393 15
            array_unshift($path, [
394 15
                'field'  => $field,
395 15
                'class'  => $class
396
            ]
397
            );
398
399 15
            $alias = $parent;
400
        }
401
402 15
        return $this->getAssociationPathValue($entity, $path);
403
    }
404
405
    /**
406
     * @param mixed $value
407
     * @param array $path
408
     *
409
     * @return array|object|null
410
     */
411 15
    private function getAssociationPathValue($value, array $path)
412
    {
413 15
        $mapping  = array_shift($path);
414 15
        $metadata = $this->em->getClassMetadata($mapping['class']);
415 15
        $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?

If you access a property on an interface, you most likely code against a concrete implementation of the interface.

Available Fixes

  1. Adding an additional type check:

    interface SomeInterface { }
    class SomeClass implements SomeInterface {
        public $a;
    }
    
    function someFunction(SomeInterface $object) {
        if ($object instanceof SomeClass) {
            $a = $object->a;
        }
    }
    
  2. Changing the type hint:

    interface SomeInterface { }
    class SomeClass implements SomeInterface {
        public $a;
    }
    
    function someFunction(SomeClass $object) {
        $a = $object->a;
    }
    
Loading history...
416 15
        $value    = $metadata->getFieldValue($value, $mapping['field']);
417
418 15
        if ($value === null) {
419 1
            return null;
420
        }
421
422 14
        if (empty($path)) {
423 14
            return $value;
424
        }
425
426
        // Handle *-to-one associations
427 3
        if ($assoc['type'] & ClassMetadata::TO_ONE) {
428 1
            return $this->getAssociationPathValue($value, $path);
429
        }
430
431 2
        $values = [];
432
433 2
        foreach ($value as $item) {
434 2
            $values[] = $this->getAssociationPathValue($item, $path);
435
        }
436
437 2
        return $values;
438
    }
439
440
    /**
441
     * {@inheritdoc}
442
     */
443 48
    public function clear()
444
    {
445 48
        return $this->region->evictAll();
446
    }
447
448
    /**
449
     * {@inheritdoc}
450
     */
451 28
    public function getRegion()
452
    {
453 28
        return $this->region;
454
    }
455
}
456