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

DefaultQueryCache::put()   D

Complexity

Conditions 20
Paths 24

Size

Total Lines 92
Code Lines 46

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 46
CRAP Score 20.0037

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 20
eloc 46
c 1
b 0
f 0
nop 4
dl 0
loc 92
ccs 46
cts 47
cp 0.9787
crap 20.0037
rs 4.1666
nc 24

How to fix   Long Method    Complexity   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

1
<?php
2
3
/*
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