Failed Conditions
Push — master ( 2ade86...13f838 )
by Jonathan
18s
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 86
    public function __construct(EntityManagerInterface $em, Region $region)
76
    {
77 86
        $cacheConfig = $em->getConfiguration()->getSecondLevelCacheConfiguration();
78
79 86
        $this->em           = $em;
80 86
        $this->region       = $region;
81 86
        $this->uow          = $em->getUnitOfWork();
82 86
        $this->cacheLogger  = $cacheConfig->getCacheLogger();
83 86
        $this->validator    = $cacheConfig->getQueryValidator();
84 86
    }
85
86
    /**
87
     * {@inheritdoc}
88
     */
89 52
    public function get(QueryCacheKey $key, ResultSetMapping $rsm, array $hints = [])
90
    {
91 52
        if ( ! ($key->cacheMode & Cache::MODE_GET)) {
92 3
            return null;
93
        }
94
95 51
        $cacheEntry = $this->region->get($key);
96
97 51
        if ( ! $cacheEntry instanceof QueryCacheEntry) {
98 47
            return null;
99
        }
100
101 38
        if ( ! $this->validator->isValid($key, $cacheEntry)) {
102 8
            $this->region->evict($key);
103
104 8
            return null;
105
        }
106
107 33
        $result      = [];
108 33
        $entityName  = reset($rsm->aliasMap);
109 33
        $hasRelation = ( ! empty($rsm->relationMap));
110 33
        $persister   = $this->uow->getEntityPersister($entityName);
111 33
        $region      = $persister->getCacheRegion();
112 33
        $regionName  = $region->getName();
113
114 33
        $cm = $this->em->getClassMetadata($entityName);
115
116 33
        $generateKeys = function (array $entry) use ($cm): EntityCacheKey {
117 33
            return new EntityCacheKey($cm->rootEntityName, $entry['identifier']);
118 33
        };
119
120 33
        $cacheKeys = new CollectionCacheEntry(array_map($generateKeys, $cacheEntry->result));
121 33
        $entries   = $region->getMultiple($cacheKeys);
122
123
        // @TODO - move to cache hydration component
124 33
        foreach ($cacheEntry->result as $index => $entry) {
125 33
            $entityEntry = is_array($entries) && array_key_exists($index, $entries) ? $entries[$index] : null;
126
127 33
            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 31
            if ($this->cacheLogger !== null) {
136 29
                $this->cacheLogger->entityCacheHit($regionName, $cacheKeys->identifiers[$index]);
137
            }
138
139 31
            if ( ! $hasRelation) {
140 22
                $result[$index]  = $this->uow->createEntity($entityEntry->class, $entityEntry->resolveAssociationEntries($this->em), self::$hints);
141
142 22
                continue;
143
            }
144
145 9
            $data = $entityEntry->data;
146
147 9
            foreach ($entry['associations'] as $name => $assoc) {
148 9
                $assocPersister  = $this->uow->getEntityPersister($assoc['targetEntity']);
149 9
                $assocRegion     = $assocPersister->getCacheRegion();
150 9
                $assocMetadata   = $this->em->getClassMetadata($assoc['targetEntity']);
151
152 9
                if ($assoc['type'] & ClassMetadata::TO_ONE) {
153
154 5
                    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 4
                    $data[$name] = $this->uow->createEntity($assocEntry->class, $assocEntry->resolveAssociationEntries($this->em), self::$hints);
166
167 4
                    if ($this->cacheLogger !== null) {
168 4
                        $this->cacheLogger->entityCacheHit($assocRegion->getName(), $assocKey);
169
                    }
170
171 4
                    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 7
            foreach ($data as $fieldName => $unCachedAssociationData) {
214
                // In some scenarios, such as EAGER+ASSOCIATION+ID+CACHE, the
215
                // cache key information in `$cacheEntry` will not contain details
216
                // for fields that are associations.
217
                //
218
                // This means that `$data` keys for some associations that may
219
                // actually not be cached will not be converted to actual association
220
                // data, yet they contain L2 cache AssociationCacheEntry objects.
221
                //
222
                // We need to unwrap those associations into proxy references,
223
                // since we don't have actual data for them except for identifiers.
224 7
                if ($unCachedAssociationData instanceof AssociationCacheEntry) {
225 4
                    $data[$fieldName] = $this->em->getReference(
226 4
                        $unCachedAssociationData->class,
227 7
                        $unCachedAssociationData->identifier
228
                    );
229
                }
230
            }
231
232 7
            $result[$index] = $this->uow->createEntity($entityEntry->class, $data, self::$hints);
233
        }
234
235 29
        $this->uow->hydrationComplete();
236
237 29
        return $result;
238
    }
239
240
    /**
241
     * {@inheritdoc}
242
     */
243 58
    public function put(QueryCacheKey $key, ResultSetMapping $rsm, $result, array $hints = [])
244
    {
245 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...
246 1
            throw new CacheException("Second level cache does not support scalar results.");
247
        }
248
249 57
        if (count($rsm->entityMappings) > 1) {
250 1
            throw new CacheException("Second level cache does not support multiple root entities.");
251
        }
252
253 56
        if ( ! $rsm->isSelect) {
254 2
            throw new CacheException("Second-level cache query supports only select statements.");
255
        }
256
257 54
        if (isset($hints[Query::HINT_FORCE_PARTIAL_LOAD]) && $hints[Query::HINT_FORCE_PARTIAL_LOAD]) {
258 1
            throw new CacheException("Second level cache does not support partial entities.");
259
        }
260
261 53
        if ( ! ($key->cacheMode & Cache::MODE_PUT)) {
262 3
            return false;
263
        }
264
265 52
        $data        = [];
266 52
        $entityName  = reset($rsm->aliasMap);
267 52
        $rootAlias   = key($rsm->aliasMap);
268 52
        $persister   = $this->uow->getEntityPersister($entityName);
269
270 52
        if ( ! ($persister instanceof CachedPersister)) {
271 1
            throw CacheException::nonCacheableEntity($entityName);
272
        }
273
274 51
        $region = $persister->getCacheRegion();
275
276 51
        foreach ($result as $index => $entity) {
277 51
            $identifier                     = $this->uow->getEntityIdentifier($entity);
278 51
            $entityKey                      = new EntityCacheKey($entityName, $identifier);
279
280 51
            if (($key->cacheMode & Cache::MODE_REFRESH) || ! $region->contains($entityKey)) {
281
                // Cancel put result if entity put fail
282 36
                if ( ! $persister->storeEntityCache($entity, $entityKey)) {
283 1
                    return false;
284
                }
285
            }
286
287 50
            $data[$index]['identifier']   = $identifier;
288 50
            $data[$index]['associations'] = [];
289
290
            // @TODO - move to cache hydration components
291 50
            foreach ($rsm->relationMap as $alias => $name) {
292 15
                $parentAlias  = $rsm->parentAliasMap[$alias];
293 15
                $parentClass  = $rsm->aliasMap[$parentAlias];
294 15
                $metadata     = $this->em->getClassMetadata($parentClass);
295 15
                $assoc        = $metadata->associationMappings[$name];
296 15
                $assocValue   = $this->getAssociationValue($rsm, $alias, $entity);
297
298 15
                if ($assocValue === null) {
299 1
                    continue;
300
                }
301
302
                // root entity association
303 14
                if ($rootAlias === $parentAlias) {
304
                    // Cancel put result if association put fail
305 14
                    if ( ($assocInfo = $this->storeAssociationCache($key, $assoc, $assocValue)) === null) {
306 2
                        return false;
307
                    }
308
309 12
                    $data[$index]['associations'][$name] = $assocInfo;
310
311 12
                    continue;
312
                }
313
314
                // store single nested association
315 2
                if ( ! is_array($assocValue)) {
316
                    // Cancel put result if association put fail
317 1
                    if ($this->storeAssociationCache($key, $assoc, $assocValue) === null) {
318
                        return false;
319
                    }
320
321 1
                    continue;
322
                }
323
324
                // store array of nested association
325 1
                foreach ($assocValue as $aVal) {
326
                    // Cancel put result if association put fail
327 1
                    if ($this->storeAssociationCache($key, $assoc, $aVal) === null) {
328 48
                        return false;
329
                    }
330
                }
331
            }
332
        }
333
334 48
        return $this->region->put($key, new QueryCacheEntry($data));
335
    }
336
337
    /**
338
     * @param \Doctrine\ORM\Cache\QueryCacheKey $key
339
     * @param array                             $assoc
340
     * @param mixed                             $assocValue
341
     *
342
     * @return array|null
343
     */
344 14
    private function storeAssociationCache(QueryCacheKey $key, array $assoc, $assocValue)
345
    {
346 14
        $assocPersister = $this->uow->getEntityPersister($assoc['targetEntity']);
347 14
        $assocMetadata  = $assocPersister->getClassMetadata();
348 14
        $assocRegion    = $assocPersister->getCacheRegion();
349
350
        // Handle *-to-one associations
351 14
        if ($assoc['type'] & ClassMetadata::TO_ONE) {
352 8
            $assocIdentifier = $this->uow->getEntityIdentifier($assocValue);
353 8
            $entityKey       = new EntityCacheKey($assocMetadata->rootEntityName, $assocIdentifier);
354
355 8
            if ( ! $assocValue instanceof Proxy && ($key->cacheMode & Cache::MODE_REFRESH) || ! $assocRegion->contains($entityKey)) {
356
                // Entity put fail
357 7
                if ( ! $assocPersister->storeEntityCache($assocValue, $entityKey)) {
358 1
                    return null;
359
                }
360
            }
361
362
            return [
363 7
                'targetEntity'  => $assocMetadata->rootEntityName,
364 7
                'identifier'    => $assocIdentifier,
365 7
                'type'          => $assoc['type']
366
            ];
367
        }
368
369
        // Handle *-to-many associations
370 6
        $list = [];
371
372 6
        foreach ($assocValue as $assocItemIndex => $assocItem) {
373 6
            $assocIdentifier = $this->uow->getEntityIdentifier($assocItem);
374 6
            $entityKey       = new EntityCacheKey($assocMetadata->rootEntityName, $assocIdentifier);
375
376 6
            if (($key->cacheMode & Cache::MODE_REFRESH) || ! $assocRegion->contains($entityKey)) {
377
                // Entity put fail
378 6
                if ( ! $assocPersister->storeEntityCache($assocItem, $entityKey)) {
379 1
                    return null;
380
                }
381
            }
382
383 5
            $list[$assocItemIndex] = $assocIdentifier;
384
        }
385
386
        return [
387 5
            'targetEntity'  => $assocMetadata->rootEntityName,
388 5
            'type'          => $assoc['type'],
389 5
            'list'          => $list,
390
        ];
391
    }
392
393
    /**
394
     * @param \Doctrine\ORM\Query\ResultSetMapping $rsm
395
     * @param string                               $assocAlias
396
     * @param object                               $entity
397
     *
398
     * @return array|object
399
     */
400 16
    private function getAssociationValue(ResultSetMapping $rsm, $assocAlias, $entity)
401
    {
402 16
        $path  = [];
403 16
        $alias = $assocAlias;
404
405 16
        while (isset($rsm->parentAliasMap[$alias])) {
406 16
            $parent = $rsm->parentAliasMap[$alias];
407 16
            $field  = $rsm->relationMap[$alias];
408 16
            $class  = $rsm->aliasMap[$parent];
409
410 16
            array_unshift($path, [
411 16
                'field'  => $field,
412 16
                'class'  => $class
413
            ]
414
            );
415
416 16
            $alias = $parent;
417
        }
418
419 16
        return $this->getAssociationPathValue($entity, $path);
420
    }
421
422
    /**
423
     * @param mixed $value
424
     * @param array $path
425
     *
426
     * @return array|object|null
427
     */
428 16
    private function getAssociationPathValue($value, array $path)
429
    {
430 16
        $mapping  = array_shift($path);
431 16
        $metadata = $this->em->getClassMetadata($mapping['class']);
432 16
        $assoc    = $metadata->associationMappings[$mapping['field']];
433 16
        $value    = $metadata->getFieldValue($value, $mapping['field']);
434
435 16
        if ($value === null) {
436 1
            return null;
437
        }
438
439 15
        if (empty($path)) {
440 15
            return $value;
441
        }
442
443
        // Handle *-to-one associations
444 3
        if ($assoc['type'] & ClassMetadata::TO_ONE) {
445 1
            return $this->getAssociationPathValue($value, $path);
446
        }
447
448 2
        $values = [];
449
450 2
        foreach ($value as $item) {
451 2
            $values[] = $this->getAssociationPathValue($item, $path);
452
        }
453
454 2
        return $values;
455
    }
456
457
    /**
458
     * {@inheritdoc}
459
     */
460 48
    public function clear()
461
    {
462 48
        return $this->region->evictAll();
463
    }
464
465
    /**
466
     * {@inheritdoc}
467
     */
468 28
    public function getRegion()
469
    {
470 28
        return $this->region;
471
    }
472
}
473