Completed
Pull Request — master (#5856)
by Fabio
16:00 queued 08:04
created

DefaultQueryCache::getAssociationValue()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 20
Code Lines 12

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 12
CRAP Score 2

Importance

Changes 1
Bugs 1 Features 0
Metric Value
c 1
b 1
f 0
dl 0
loc 20
ccs 12
cts 12
cp 1
rs 9.4285
cc 2
eloc 12
nc 2
nop 3
crap 2
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 = array(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 81
    public function __construct(EntityManagerInterface $em, Region $region)
76
    {
77 81
        $cacheConfig = $em->getConfiguration()->getSecondLevelCacheConfiguration();
78
79 81
        $this->em           = $em;
80 81
        $this->region       = $region;
81 81
        $this->uow          = $em->getUnitOfWork();
82 81
        $this->cacheLogger  = $cacheConfig->getCacheLogger();
83 81
        $this->validator    = $cacheConfig->getQueryValidator();
84 81
    }
85
86
    /**
87
     * {@inheritdoc}
88
     */
89 47
    public function get(QueryCacheKey $key, ResultSetMapping $rsm, array $hints = array())
90
    {
91 47
        if ( ! ($key->cacheMode & Cache::MODE_GET)) {
92 3
            return null;
93
        }
94
95 46
        $entry = $this->region->get($key);
96
97 46
        if ( ! $entry instanceof QueryCacheEntry) {
98 43
            return null;
99
        }
100
101 33
        if ( ! $this->validator->isValid($key, $entry)) {
102 2
            $this->region->evict($key);
103
104 2
            return null;
105
        }
106
107 31
        $result      = array();
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
        // @TODO - move to cache hydration component
116 31
        foreach ($entry->result as $index => $entry) {
117
118 31
            if (($entityEntry = $region->get($entityKey = new EntityCacheKey($cm->rootEntityName, $entry['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?

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...
119
120 2
                if ($this->cacheLogger !== null) {
121 1
                    $this->cacheLogger->entityCacheMiss($regionName, $entityKey);
122
                }
123
124 2
                return null;
125
            }
126
127 29
            if ($this->cacheLogger !== null) {
128 28
                $this->cacheLogger->entityCacheHit($regionName, $entityKey);
129
            }
130
131 29
            if ( ! $hasRelation) {
132
133 21
                $result[$index]  = $this->uow->createEntity($entityEntry->class, $entityEntry->resolveAssociationEntries($this->em), self::$hints);
134
135 21
                continue;
136
            }
137
138 8
            $data = $entityEntry->data;
139
140 8
            foreach ($entry['associations'] as $name => $assoc) {
141
142 8
                $assocPersister  = $this->uow->getEntityPersister($assoc['targetEntity']);
143 8
                $assocRegion     = $assocPersister->getCacheRegion();
144
145 8
                if ($assoc['type'] & ClassMetadata::TO_ONE) {
146
147 4
                    if (($assocEntry = $assocRegion->get($assocKey = new EntityCacheKey($assoc['targetEntity'], $assoc['identifier']))) === null) {
148
149 1
                        if ($this->cacheLogger !== null) {
150 1
                            $this->cacheLogger->entityCacheMiss($assocRegion->getName(), $assocKey);
151
                        }
152
153 1
                        $this->uow->hydrationComplete();
154
155 1
                        return null;
156
                    }
157
158 3
                    $data[$name] = $this->uow->createEntity($assocEntry->class, $assocEntry->resolveAssociationEntries($this->em), self::$hints);
159
160 3
                    if ($this->cacheLogger !== null) {
161 3
                        $this->cacheLogger->entityCacheHit($assocRegion->getName(), $assocKey);
162
                    }
163
164 3
                    continue;
165
                }
166
167 4
                if ( ! isset($assoc['list']) || empty($assoc['list'])) {
168
                    continue;
169
                }
170
171 4
                $targetClass = $this->em->getClassMetadata($assoc['targetEntity']);
172 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...
173
174 4
                foreach ($assoc['list'] as $assocIndex => $assocId) {
175
176 4
                    if (($assocEntry = $assocRegion->get($assocKey = new EntityCacheKey($assoc['targetEntity'], $assocId))) === null) {
177
178 1
                        if ($this->cacheLogger !== null) {
179 1
                            $this->cacheLogger->entityCacheMiss($assocRegion->getName(), $assocKey);
180
                        }
181
182 1
                        $this->uow->hydrationComplete();
183
184 1
                        return null;
185
                    }
186
187 3
                    $element = $this->uow->createEntity($assocEntry->class, $assocEntry->resolveAssociationEntries($this->em), self::$hints);
188
189 3
                    $collection->hydrateSet($assocIndex, $element);
190
191 3
                    if ($this->cacheLogger !== null) {
192 3
                        $this->cacheLogger->entityCacheHit($assocRegion->getName(), $assocKey);
193
                    }
194
                }
195
196 3
                $data[$name] = $collection;
197
198 3
                $collection->setInitialized(true);
199
            }
200
201 6
            $result[$index] = $this->uow->createEntity($entityEntry->class, $data, self::$hints);
202
        }
203
204 27
        $this->uow->hydrationComplete();
205
206 27
        return $result;
207
    }
208
209
    /**
210
     * {@inheritdoc}
211
     */
212 54
    public function put(QueryCacheKey $key, ResultSetMapping $rsm, $result, array $hints = array())
213
    {
214 54
        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...
215 1
            throw new CacheException("Second level cache does not support scalar results.");
216
        }
217
218 53
        if (count($rsm->entityMappings) > 1) {
219 1
            throw new CacheException("Second level cache does not support multiple root entities.");
220
        }
221
222 52
        if ( ! $rsm->isSelect) {
223 2
            throw new CacheException("Second-level cache query supports only select statements.");
224
        }
225
226 50
        if (isset($hints[Query::HINT_FORCE_PARTIAL_LOAD]) && $hints[Query::HINT_FORCE_PARTIAL_LOAD]) {
227 1
            throw new CacheException("Second level cache does not support partial entities.");
228
        }
229
230 49
        if ( ! ($key->cacheMode & Cache::MODE_PUT)) {
231 3
            return false;
232
        }
233
234 48
        $data        = array();
235 48
        $entityName  = reset($rsm->aliasMap);
236 48
        $rootAlias   = key($rsm->aliasMap);
237 48
        $hasRelation = ( ! empty($rsm->relationMap));
238 48
        $persister   = $this->uow->getEntityPersister($entityName);
239
240 48
        if ( ! ($persister instanceof CachedPersister)) {
241 1
            throw CacheException::nonCacheableEntity($entityName);
242
        }
243
244 47
        $region = $persister->getCacheRegion();
245
246 47
        foreach ($result as $index => $entity) {
247 47
            $identifier                     = $this->uow->getEntityIdentifier($entity);
248 47
            $entityKey                      = new EntityCacheKey($entityName, $identifier);
249 47
            $data[$index]['identifier']     = $identifier;
250 47
            $data[$index]['associations']   = array();
251
252 47
            if (($key->cacheMode & Cache::MODE_REFRESH) || ! $region->contains($entityKey)) {
253
                // Cancel put result if entity put fail
254 35
                if ( ! $persister->storeEntityCache($entity, $entityKey)) {
255 1
                    return false;
256
                }
257
            }
258
259 46
            if ( ! $hasRelation) {
260 32
                continue;
261
            }
262
263
            // @TODO - move to cache hydration components
264 14
            foreach ($rsm->relationMap as $alias => $name) {
265 14
                $parentAlias  = $rsm->parentAliasMap[$alias];
266 14
                $parentClass  = $rsm->aliasMap[$parentAlias];
267 14
                $metadata     = $this->em->getClassMetadata($parentClass);
268 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...
269 14
                $assocValue   = $this->getAssociationValue($rsm, $alias, $entity);
270
271 14
                if ($assocValue === null) {
272 1
                    continue;
273
                }
274
275
                // root entity association
276 13
                if ($rootAlias === $parentAlias) {
277
                    // Cancel put result if association put fail
278 13
                    if ( ($assocInfo = $this->storeAssociationCache($key, $assoc, $assocValue)) === null) {
279 2
                        return false;
280
                    }
281
282 11
                    $data[$index]['associations'][$name] = $assocInfo;
283
284 11
                    continue;
285
                }
286
287
                // store single nested association
288 2
                if ( ! is_array($assocValue)) {
289
                    // Cancel put result if association put fail
290 1
                    if ($this->storeAssociationCache($key, $assoc, $assocValue) === null) {
291
                        return false;
292
                    }
293
294 1
                    continue;
295
                }
296
297
                // store array of nested association
298 1
                foreach ($assocValue as $aVal) {
299
                    // Cancel put result if association put fail
300 1
                    if ($this->storeAssociationCache($key, $assoc, $aVal) === null) {
301 12
                        return false;
302
                    }
303
                }
304
            }
305
        }
306
307 44
        return $this->region->put($key, new QueryCacheEntry($data));
308
    }
309
310
    /**
311
     * @param \Doctrine\ORM\Cache\QueryCacheKey $key
312
     * @param array                             $assoc
313
     * @param mixed                             $assocValue
314
     *
315
     * @return array|null
316
     */
317 13
    private function storeAssociationCache(QueryCacheKey $key, array $assoc, $assocValue)
318
    {
319 13
        $assocPersister = $this->uow->getEntityPersister($assoc['targetEntity']);
320 13
        $assocMetadata  = $assocPersister->getClassMetadata();
321 13
        $assocRegion    = $assocPersister->getCacheRegion();
322
323
        // Handle *-to-one associations
324 13
        if ($assoc['type'] & ClassMetadata::TO_ONE) {
325 7
            $assocIdentifier = $this->uow->getEntityIdentifier($assocValue);
326 7
            $entityKey       = new EntityCacheKey($assocMetadata->rootEntityName, $assocIdentifier);
327
328 7
            if ( ! $assocValue instanceof Proxy && ($key->cacheMode & Cache::MODE_REFRESH) || ! $assocRegion->contains($entityKey)) {
329
                // Entity put fail
330 7
                if ( ! $assocPersister->storeEntityCache($assocValue, $entityKey)) {
331 1
                    return null;
332
                }
333
            }
334
335
            return array(
336 6
                'targetEntity'  => $assocMetadata->rootEntityName,
337 6
                'identifier'    => $assocIdentifier,
338 6
                'type'          => $assoc['type']
339
            );
340
        }
341
342
        // Handle *-to-many associations
343 6
        $list = array();
344
345 6
        foreach ($assocValue as $assocItemIndex => $assocItem) {
346 6
            $assocIdentifier = $this->uow->getEntityIdentifier($assocItem);
347 6
            $entityKey       = new EntityCacheKey($assocMetadata->rootEntityName, $assocIdentifier);
348
349 6
            if (($key->cacheMode & Cache::MODE_REFRESH) || ! $assocRegion->contains($entityKey)) {
350
                // Entity put fail
351 6
                if ( ! $assocPersister->storeEntityCache($assocItem, $entityKey)) {
352 1
                    return null;
353
                }
354
            }
355
356 5
            $list[$assocItemIndex] = $assocIdentifier;
357
        }
358
359
        return array(
360 5
            'targetEntity'  => $assocMetadata->rootEntityName,
361 5
            'type'          => $assoc['type'],
362 5
            'list'          => $list,
363
        );
364
    }
365
366
    /**
367
     * @param \Doctrine\ORM\Query\ResultSetMapping $rsm
368
     * @param string                               $assocAlias
369
     * @param object                               $entity
370
     *
371
     * @return array|object
372
     */
373 15
    private function getAssociationValue(ResultSetMapping $rsm, $assocAlias, $entity)
374
    {
375 15
        $path  = array();
376 15
        $alias = $assocAlias;
377
378 15
        while (isset($rsm->parentAliasMap[$alias])) {
379 15
            $parent = $rsm->parentAliasMap[$alias];
380 15
            $field  = $rsm->relationMap[$alias];
381 15
            $class  = $rsm->aliasMap[$parent];
382
383 15
            array_unshift($path, array(
384 15
                'field'  => $field,
385 15
                'class'  => $class
386
            ));
387
388 15
            $alias = $parent;
389
        }
390
391 15
        return $this->getAssociationPathValue($entity, $path);
392
    }
393
394
    /**
395
     * @param mixed $value
396
     * @param array $path
397
     *
398
     * @return array|object|null
399
     */
400 15
    private function getAssociationPathValue($value, array $path)
401
    {
402 15
        $mapping  = array_shift($path);
403 15
        $metadata = $this->em->getClassMetadata($mapping['class']);
404 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...
405 15
        $value    = $metadata->getFieldValue($value, $mapping['field']);
406
407 15
        if ($value === null) {
408 1
            return null;
409
        }
410
411 14
        if (empty($path)) {
412 14
            return $value;
413
        }
414
415
        // Handle *-to-one associations
416 3
        if ($assoc['type'] & ClassMetadata::TO_ONE) {
417 1
            return $this->getAssociationPathValue($value, $path);
418
        }
419
420 2
        $values = array();
421
422 2
        foreach ($value as $item) {
423 2
            $values[] = $this->getAssociationPathValue($item, $path);
424
        }
425
426 2
        return $values;
427
    }
428
429
    /**
430
     * {@inheritdoc}
431
     */
432 47
    public function clear()
433
    {
434 47
        return $this->region->evictAll();
435
    }
436
437
    /**
438
     * {@inheritdoc}
439
     */
440 25
    public function getRegion()
441
    {
442 25
        return $this->region;
443
    }
444
}
445