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