Failed Conditions
Pull Request — master (#6743)
by Grégoire
05:37
created

DefaultQueryCache::getAssociationPathValue()   B

Complexity

Conditions 5
Paths 5

Size

Total Lines 27
Code Lines 14

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 15
CRAP Score 5

Importance

Changes 0
Metric Value
cc 5
eloc 14
nc 5
nop 2
dl 0
loc 27
ccs 15
cts 15
cp 1
crap 5
rs 8.439
c 0
b 0
f 0
1
<?php
2
3
declare(strict_types=1);
4
5
namespace Doctrine\ORM\Cache;
6
7
use Doctrine\ORM\Cache;
8
use Doctrine\ORM\Cache\Persister\CachedPersister;
9
use Doctrine\ORM\EntityManagerInterface;
10
use Doctrine\ORM\Mapping\AssociationMetadata;
11
use Doctrine\ORM\Mapping\ToOneAssociationMetadata;
12
use Doctrine\Common\Collections\ArrayCollection;
13
use Doctrine\Common\Proxy\Proxy;
14
use Doctrine\ORM\Cache;
0 ignored issues
show
Bug introduced by
A parse error occurred: Cannot use Doctrine\ORM\Cache as Cache because the name is already in use
Loading history...
15
use Doctrine\ORM\Cache\FeatureNotImplemented;
16
use Doctrine\ORM\Cache\NonCacheableEntity;
17
use Doctrine\ORM\Cache\Persister\CachedPersister;
18
use Doctrine\ORM\EntityManagerInterface;
19
use Doctrine\ORM\Mapping\AssociationMetadata;
20
use Doctrine\ORM\Mapping\ClassMetadata;
21
use Doctrine\ORM\Mapping\ToOneAssociationMetadata;
22
use Doctrine\ORM\PersistentCollection;
23
use Doctrine\ORM\Query;
24
use Doctrine\ORM\Query\ResultSetMapping;
25
use ProxyManager\Proxy\GhostObjectInterface;
26
27
/**
28
 * Default query cache implementation.
29
 */
30
class DefaultQueryCache implements QueryCache
31
{
32
    /**
33
     * @var \Doctrine\ORM\EntityManagerInterface
34
     */
35
    private $em;
36
37
    /**
38
     * @var \Doctrine\ORM\Cache\Region
39
     */
40
    private $region;
41
42
    /**
43
     * @var \Doctrine\ORM\Cache\QueryCacheValidator
44
     */
45
    private $validator;
46
47
    /**
48
     * @var \Doctrine\ORM\Cache\Logging\CacheLogger
49
     */
50 86
    protected $cacheLogger;
51
52 86
    /**
53
     * @var mixed[]
54 86
     */
55 86
    private static $hints = [Query::HINT_CACHE_ENABLED => true];
56 86
57 86
    /**
58 86
     * @param \Doctrine\ORM\EntityManagerInterface $em     The entity manager.
59
     * @param \Doctrine\ORM\Cache\Region           $region The query region.
60
     */
61
    public function __construct(EntityManagerInterface $em, Region $region)
62
    {
63 52
        $cacheConfig = $em->getConfiguration()->getSecondLevelCacheConfiguration();
64
65 52
        $this->em          = $em;
66 3
        $this->region      = $region;
67
        $this->cacheLogger = $cacheConfig->getCacheLogger();
68
        $this->validator   = $cacheConfig->getQueryValidator();
69 51
    }
70
71 51
    /**
72 47
     * {@inheritdoc}
73
     */
74
    public function get(QueryCacheKey $key, ResultSetMapping $rsm, array $hints = [])
75 38
    {
76 8
        if (! ($key->cacheMode & Cache::MODE_GET)) {
77
            return null;
78 8
        }
79
80
        $cacheEntry = $this->region->get($key);
81 33
82 33
        if (! $cacheEntry instanceof QueryCacheEntry) {
83 33
            return null;
84 33
        }
85 33
86 33
        if (! $this->validator->isValid($key, $cacheEntry)) {
87 33
            $this->region->evict($key);
88
89 33
            return null;
90
        }
91 33
92 33
        $result      = [];
93 33
        $entityName  = reset($rsm->aliasMap);
94
        $hasRelation = ( ! empty($rsm->relationMap));
95 33
        $unitOfWork  = $this->em->getUnitOfWork();
96 33
        $persister   = $unitOfWork->getEntityPersister($entityName);
97
        $region      = $persister->getCacheRegion();
98
        $regionName  = $region->getName();
99 33
100 33
        $cm = $this->em->getClassMetadata($entityName);
101
102 33
        $generateKeys = function (array $entry) use ($cm) : EntityCacheKey {
103 2
            return new EntityCacheKey($cm->getRootClassName(), $entry['identifier']);
104 1
        };
105
106
        $cacheKeys = new CollectionCacheEntry(array_map($generateKeys, $cacheEntry->result));
107 2
        $entries   = $region->getMultiple($cacheKeys);
108
109
        // @TODO - move to cache hydration component
110 31
        foreach ($cacheEntry->result as $index => $entry) {
111 29
            $entityEntry = is_array($entries) ? ($entries[$index] ?? null) : null;
112
113
            if ($entityEntry === null) {
114 31
                if ($this->cacheLogger !== null) {
115 22
                    $this->cacheLogger->entityCacheMiss($regionName, $cacheKeys->identifiers[$index]);
116 22
                }
117 22
118 22
                return null;
119
            }
120
121 22
            if ($this->cacheLogger !== null) {
122
                $this->cacheLogger->entityCacheHit($regionName, $cacheKeys->identifiers[$index]);
123
            }
124 9
125
            if (! $hasRelation) {
126 9
                $result[$index] = $unitOfWork->createEntity(
127 9
                    $entityEntry->class,
128 9
                    $entityEntry->resolveAssociationEntries($this->em),
129 9
                    self::$hints
130
                );
131
132 9
                continue;
133 5
            }
134
135 5
            $data = $entityEntry->data;
136 5
137 1
            foreach ($entry['associations'] as $name => $assoc) {
138 1
                $assocPersister = $unitOfWork->getEntityPersister($assoc['targetEntity']);
139
                $assocRegion    = $assocPersister->getCacheRegion();
140
                $assocMetadata  = $this->em->getClassMetadata($assoc['targetEntity']);
141 1
142
                // *-to-one association
143 1
                if (isset($assoc['identifier'])) {
144
                    $assocKey = new EntityCacheKey($assocMetadata->getRootClassName(), $assoc['identifier']);
145
146 4
                    $assocEntry = $assocRegion->get($assocKey);
147 4
                    if ($assocEntry === null) {
148 4
                        if ($this->cacheLogger !== null) {
149 4
                            $this->cacheLogger->entityCacheMiss($assocRegion->getName(), $assocKey);
150
                        }
151
152 4
                        $unitOfWork->hydrationComplete();
153 4
154
                        return null;
155
                    }
156 4
157
                    $data[$name] = $unitOfWork->createEntity(
158
                        $assocEntry->class,
159 4
                        $assocEntry->resolveAssociationEntries($this->em),
160
                        self::$hints
161
                    );
162
163 4
                    if ($this->cacheLogger !== null) {
164 4
                        $this->cacheLogger->entityCacheHit($assocRegion->getName(), $assocKey);
165 4
                    }
166
167 4
                    continue;
168 4
                }
169
170
                if (! isset($assoc['list']) || empty($assoc['list'])) {
171 4
                    continue;
172
                }
173 4
174 4
                $generateKeys = function ($id) use ($assocMetadata) : EntityCacheKey {
175
                    return new EntityCacheKey($assocMetadata->getRootClassName(), $id);
176 4
                };
177 1
178 1
                $assocKeys    = new CollectionCacheEntry(array_map($generateKeys, $assoc['list']));
179
                $assocEntries = $assocRegion->getMultiple($assocKeys);
180
181 1
                // *-to-many association
182
                $collection = [];
183 1
184
                foreach ($assoc['list'] as $assocIndex => $assocId) {
185
                    $assocEntry = is_array($assocEntries) ? ($assocEntries[$assocIndex] ?? null) : null;
186 3
187 3
                    if ($assocEntry === null) {
188 3
                        if ($this->cacheLogger !== null) {
189 3
                            $this->cacheLogger->entityCacheMiss($assocRegion->getName(), $assocKeys->identifiers[$assocIndex]);
190
                        }
191
192 3
                        $unitOfWork->hydrationComplete();
193 3
194
                        return null;
195
                    }
196
197 3
                    $collection[$assocIndex] = $unitOfWork->createEntity(
198
                        $assocEntry->class,
199
                        $assocEntry->resolveAssociationEntries($this->em),
200 7
                        self::$hints
201
                    );
202
203
                    if ($this->cacheLogger !== null) {
204
                        $this->cacheLogger->entityCacheHit($assocRegion->getName(), $assocKeys->identifiers[$assocIndex]);
205
                    }
206
                }
207
208
                $data[$name] = $collection;
209
            }
210
211 7
            foreach ($data as $fieldName => $unCachedAssociationData) {
212 4
                // In some scenarios, such as EAGER+ASSOCIATION+ID+CACHE, the
213 4
                // cache key information in `$cacheEntry` will not contain details
214 7
                // for fields that are associations.
215
                //
216
                // This means that `$data` keys for some associations that may
217
                // actually not be cached will not be converted to actual association
218
                // data, yet they contain L2 cache AssociationCacheEntry objects.
219 7
                //
220
                // We need to unwrap those associations into proxy references,
221
                // since we don't have actual data for them except for identifiers.
222 29
                if ($unCachedAssociationData instanceof AssociationCacheEntry) {
223
                    $data[$fieldName] = $this->em->getReference(
224 29
                        $unCachedAssociationData->class,
225
                        $unCachedAssociationData->identifier
226
                    );
227
                }
228
            }
229
230
            $result[$index] = $unitOfWork->createEntity($entityEntry->class, $data, self::$hints);
231 58
        }
232
233 58
        $unitOfWork->hydrationComplete();
234 1
235
        return $result;
236
    }
237 57
238 1
    /**
239
     * {@inheritdoc}
240
     * @param mixed[] $hints
241 56
     */
242 2
    public function put(QueryCacheKey $key, ResultSetMapping $rsm, $result, array $hints = [])
243
    {
244
        if ($rsm->scalarMappings) {
245 54
            throw FeatureNotImplemented::scalarResults();
246 1
        }
247
248
        if (count($rsm->entityMappings) > 1) {
249 53
            throw FeatureNotImplemented::multipleRootEntities();
250 3
        }
251
252
        if ( ! $rsm->isSelect) {
253 52
            throw FeatureNotImplemented::nonSelectStatements();
254 52
        }
255 52
256 52
        if (isset($hints[Query::HINT_FORCE_PARTIAL_LOAD]) && $hints[Query::HINT_FORCE_PARTIAL_LOAD]) {
257 52
            throw FeatureNotImplemented::partialEntities();
258
        }
259 52
260 1
        if (! ($key->cacheMode & Cache::MODE_PUT)) {
261
            return false;
262
        }
263 51
264
        $data       = [];
265 51
        $entityName = reset($rsm->aliasMap);
266 51
        $rootAlias  = key($rsm->aliasMap);
267 51
        $unitOfWork = $this->em->getUnitOfWork();
268
        $persister  = $unitOfWork->getEntityPersister($entityName);
269 51
270
        if (! ($persister instanceof CachedPersister)) {
271 36
            throw NonCacheableEntity::fromEntity($entityName);
272 1
        }
273
274
        $region = $persister->getCacheRegion();
275
276 50
        foreach ($result as $index => $entity) {
277 50
            $identifier = $unitOfWork->getEntityIdentifier($entity);
278
            $entityKey  = new EntityCacheKey($entityName, $identifier);
279
280 50
            if (($key->cacheMode & Cache::MODE_REFRESH) || ! $region->contains($entityKey)) {
281 15
                // Cancel put result if entity put fail
282 15
                if (! $persister->storeEntityCache($entity, $entityKey)) {
283 15
                    return false;
284 15
                }
285 15
            }
286
287 15
            $data[$index]['identifier']   = $identifier;
288 1
            $data[$index]['associations'] = [];
289
290
            // @TODO - move to cache hydration components
291
            foreach ($rsm->relationMap as $alias => $name) {
292 14
                $parentAlias = $rsm->parentAliasMap[$alias];
293
                $parentClass = $rsm->aliasMap[$parentAlias];
294 14
                $metadata    = $this->em->getClassMetadata($parentClass);
295 14
                $association = $metadata->getProperty($name);
296 2
                $assocValue  = $this->getAssociationValue($rsm, $alias, $entity);
297
298
                if ($assocValue === null) {
299 12
                    continue;
300
                }
301 12
302
                // root entity association
303
                if ($rootAlias === $parentAlias) {
304
                    // Cancel put result if association put fail
305 2
                    $assocInfo = $this->storeAssociationCache($key, $association, $assocValue);
306
                    if ($assocInfo === null) {
307 1
                        return false;
308
                    }
309
310
                    $data[$index]['associations'][$name] = $assocInfo;
311 1
312
                    continue;
313
                }
314
315 1
                // store single nested association
316
                if (! is_array($assocValue)) {
317 1
                    // Cancel put result if association put fail
318 48
                    if ($this->storeAssociationCache($key, $association, $assocValue) === null) {
319
                        return false;
320
                    }
321
322
                    continue;
323
                }
324 48
325
                // store array of nested association
326
                foreach ($assocValue as $aVal) {
327
                    // Cancel put result if association put fail
328
                    if ($this->storeAssociationCache($key, $association, $aVal) === null) {
329
                        return false;
330
                    }
331
                }
332
            }
333 14
        }
334
335 14
        return $this->region->put($key, new QueryCacheEntry($data));
336 14
    }
337 14
338 14
    /**
339
     * @param AssociationMetadata $assoc
340
     * @param mixed[]             $assocValue
341 14
     *
342 8
     * @return mixed[]|null
343 8
     */
344
    private function storeAssociationCache(QueryCacheKey $key, AssociationMetadata $association, $assocValue)
345 8
    {
346
        $unitOfWork     = $this->em->getUnitOfWork();
347 7
        $assocPersister = $unitOfWork->getEntityPersister($association->getTargetEntity());
348 1
        $assocMetadata  = $assocPersister->getClassMetadata();
349
        $assocRegion    = $assocPersister->getCacheRegion();
350
351
        // Handle *-to-one associations
352
        if ($association instanceof ToOneAssociationMetadata) {
353 7
            $assocIdentifier = $unitOfWork->getEntityIdentifier($assocValue);
354 7
            $entityKey       = new EntityCacheKey($assocMetadata->getRootClassName(), $assocIdentifier);
355
356
            if ((! $assocValue instanceof GhostObjectInterface && ($key->cacheMode & Cache::MODE_REFRESH)) || ! $assocRegion->contains($entityKey)) {
357
                // Entity put fail
358
                if (! $assocPersister->storeEntityCache($assocValue, $entityKey)) {
359 6
                    return null;
360
                }
361 6
            }
362 6
363 6
            return [
364
                'targetEntity'  => $assocMetadata->getRootClassName(),
365 6
                'identifier'    => $assocIdentifier,
366
            ];
367 6
        }
368 1
369
        // Handle *-to-many associations
370
        $list = [];
371
372 5
        foreach ($assocValue as $assocItemIndex => $assocItem) {
373
            $assocIdentifier = $unitOfWork->getEntityIdentifier($assocItem);
374
            $entityKey       = new EntityCacheKey($assocMetadata->getRootClassName(), $assocIdentifier);
375
376 5
            if (($key->cacheMode & Cache::MODE_REFRESH) || ! $assocRegion->contains($entityKey)) {
377 5
                // Entity put fail
378
                if (! $assocPersister->storeEntityCache($assocItem, $entityKey)) {
379
                    return null;
380
                }
381
            }
382
383
            $list[$assocItemIndex] = $assocIdentifier;
384
        }
385
386
        return [
387 16
            'targetEntity' => $assocMetadata->getRootClassName(),
388
            'list'         => $list,
389 16
        ];
390 16
    }
391
392 16
    /**
393 16
     * @param string $assocAlias
394 16
     * @param object $entity
395 16
     *
396
     * @return mixed[]|object
397 16
     */
398 16
    private function getAssociationValue(ResultSetMapping $rsm, $assocAlias, $entity)
399 16
    {
400
        $path  = [];
401
        $alias = $assocAlias;
402 16
403
        while (isset($rsm->parentAliasMap[$alias])) {
404
            $parent = $rsm->parentAliasMap[$alias];
405 16
            $field  = $rsm->relationMap[$alias];
406
            $class  = $rsm->aliasMap[$parent];
407
408
            array_unshift($path, [
409
                'field'  => $field,
410
                'class'  => $class,
411
            ]);
412
413
            $alias = $parent;
414 16
        }
415
416 16
        return $this->getAssociationPathValue($entity, $path);
417 16
    }
418 16
419 16
    /**
420
     * @param mixed     $value
421 16
     * @param mixed[][] $path
422 1
     *
423
     * @return mixed[]|object|null
424
     */
425 15
    private function getAssociationPathValue($value, array $path)
426 15
    {
427
        $mapping     = array_shift($path);
428
        $metadata    = $this->em->getClassMetadata($mapping['class']);
429
        $association = $metadata->getProperty($mapping['field']);
430 3
        $value       = $association->getValue($value);
431 1
432
        if ($value === null) {
433
            return null;
434 2
        }
435
436 2
        if (empty($path)) {
437 2
            return $value;
438
        }
439
440 2
        // Handle *-to-one associations
441
        if ($association instanceof ToOneAssociationMetadata) {
442
            return $this->getAssociationPathValue($value, $path);
443
        }
444
445
        $values = [];
446 48
447
        foreach ($value as $item) {
448 48
            $values[] = $this->getAssociationPathValue($item, $path);
449
        }
450
451
        return $values;
452
    }
453
454 28
    /**
455
     * {@inheritdoc}
456 28
     */
457
    public function clear()
458
    {
459
        return $this->region->evictAll();
460
    }
461
462
    /**
463
     * {@inheritdoc}
464
     */
465
    public function getRegion()
466
    {
467
        return $this->region;
468
    }
469
}
470