Failed Conditions
Push — master ( 6744b4...2b8acb )
by Marco
60:45 queued 60:36
created

lib/Doctrine/ORM/Cache/DefaultQueryCache.php (1 issue)

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