Completed
Push — master ( 2a239b...205ee7 )
by Marco
22s
created

lib/Doctrine/ORM/Cache/DefaultQueryCache.php (5 issues)

Upgrade to new PHP Analysis Engine

These results are based on our legacy PHP analysis, consider migrating to our new PHP analysis engine instead. Learn more

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