From bf6655a534a6775d30cafa67bd801276bda1d98d Mon Sep 17 00:00:00 2001 From: polo Date: Tue, 13 Aug 2024 23:45:21 +0200 Subject: =?UTF-8?q?VERSION=200.2=20doctrine=20ORM=20et=20entit=C3=A9s?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../doctrine/orm/src/Cache/DefaultQueryCache.php | 414 +++++++++++++++++++++ 1 file changed, 414 insertions(+) create mode 100644 vendor/doctrine/orm/src/Cache/DefaultQueryCache.php (limited to 'vendor/doctrine/orm/src/Cache/DefaultQueryCache.php') 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 @@ + */ + private static array $hints = [Query::HINT_CACHE_ENABLED => true]; + + public function __construct( + private readonly EntityManagerInterface $em, + private readonly Region $region, + ) { + $cacheConfig = $em->getConfiguration()->getSecondLevelCacheConfiguration(); + + $this->uow = $em->getUnitOfWork(); + $this->cacheLogger = $cacheConfig->getCacheLogger(); + $this->validator = $cacheConfig->getQueryValidator(); + } + + /** + * {@inheritDoc} + */ + public function get(QueryCacheKey $key, ResultSetMapping $rsm, array $hints = []): array|null + { + if (! ($key->cacheMode & Cache::MODE_GET)) { + return null; + } + + $cacheEntry = $this->region->get($key); + + if (! $cacheEntry instanceof QueryCacheEntry) { + return null; + } + + if (! $this->validator->isValid($key, $cacheEntry)) { + $this->region->evict($key); + + return null; + } + + $result = []; + $entityName = reset($rsm->aliasMap); + $hasRelation = ! empty($rsm->relationMap); + $persister = $this->uow->getEntityPersister($entityName); + assert($persister instanceof CachedEntityPersister); + + $region = $persister->getCacheRegion(); + $regionName = $region->getName(); + + $cm = $this->em->getClassMetadata($entityName); + + $generateKeys = static fn (array $entry): EntityCacheKey => new EntityCacheKey($cm->rootEntityName, $entry['identifier']); + + $cacheKeys = new CollectionCacheEntry(array_map($generateKeys, $cacheEntry->result)); + $entries = $region->getMultiple($cacheKeys) ?? []; + + // @TODO - move to cache hydration component + foreach ($cacheEntry->result as $index => $entry) { + $entityEntry = $entries[$index] ?? null; + + if (! $entityEntry instanceof EntityCacheEntry) { + $this->cacheLogger?->entityCacheMiss($regionName, $cacheKeys->identifiers[$index]); + + return null; + } + + $this->cacheLogger?->entityCacheHit($regionName, $cacheKeys->identifiers[$index]); + + if (! $hasRelation) { + $result[$index] = $this->uow->createEntity($entityEntry->class, $entityEntry->resolveAssociationEntries($this->em), self::$hints); + + continue; + } + + $data = $entityEntry->data; + + foreach ($entry['associations'] as $name => $assoc) { + $assocPersister = $this->uow->getEntityPersister($assoc['targetEntity']); + assert($assocPersister instanceof CachedEntityPersister); + + $assocRegion = $assocPersister->getCacheRegion(); + $assocMetadata = $this->em->getClassMetadata($assoc['targetEntity']); + + if ($assoc['type'] & ClassMetadata::TO_ONE) { + $assocKey = new EntityCacheKey($assocMetadata->rootEntityName, $assoc['identifier']); + $assocEntry = $assocRegion->get($assocKey); + + if ($assocEntry === null) { + $this->cacheLogger?->entityCacheMiss($assocRegion->getName(), $assocKey); + + $this->uow->hydrationComplete(); + + return null; + } + + $data[$name] = $this->uow->createEntity($assocEntry->class, $assocEntry->resolveAssociationEntries($this->em), self::$hints); + + $this->cacheLogger?->entityCacheHit($assocRegion->getName(), $assocKey); + + continue; + } + + if (! isset($assoc['list']) || empty($assoc['list'])) { + continue; + } + + $generateKeys = static fn (array $id): EntityCacheKey => new EntityCacheKey($assocMetadata->rootEntityName, $id); + + $collection = new PersistentCollection($this->em, $assocMetadata, new ArrayCollection()); + $assocKeys = new CollectionCacheEntry(array_map($generateKeys, $assoc['list'])); + $assocEntries = $assocRegion->getMultiple($assocKeys); + + foreach ($assoc['list'] as $assocIndex => $assocId) { + $assocEntry = is_array($assocEntries) ? ($assocEntries[$assocIndex] ?? null) : null; + + if ($assocEntry === null) { + $this->cacheLogger?->entityCacheMiss($assocRegion->getName(), $assocKeys->identifiers[$assocIndex]); + + $this->uow->hydrationComplete(); + + return null; + } + + $element = $this->uow->createEntity($assocEntry->class, $assocEntry->resolveAssociationEntries($this->em), self::$hints); + + $collection->hydrateSet($assocIndex, $element); + + $this->cacheLogger?->entityCacheHit($assocRegion->getName(), $assocKeys->identifiers[$assocIndex]); + } + + $data[$name] = $collection; + + $collection->setInitialized(true); + } + + foreach ($data as $fieldName => $unCachedAssociationData) { + // In some scenarios, such as EAGER+ASSOCIATION+ID+CACHE, the + // cache key information in `$cacheEntry` will not contain details + // for fields that are associations. + // + // This means that `$data` keys for some associations that may + // actually not be cached will not be converted to actual association + // data, yet they contain L2 cache AssociationCacheEntry objects. + // + // We need to unwrap those associations into proxy references, + // since we don't have actual data for them except for identifiers. + if ($unCachedAssociationData instanceof AssociationCacheEntry) { + $data[$fieldName] = $this->em->getReference( + $unCachedAssociationData->class, + $unCachedAssociationData->identifier, + ); + } + } + + $result[$index] = $this->uow->createEntity($entityEntry->class, $data, self::$hints); + } + + $this->uow->hydrationComplete(); + + return $result; + } + + /** + * {@inheritDoc} + */ + public function put(QueryCacheKey $key, ResultSetMapping $rsm, mixed $result, array $hints = []): bool + { + if ($rsm->scalarMappings) { + throw FeatureNotImplemented::scalarResults(); + } + + if (count($rsm->entityMappings) > 1) { + throw FeatureNotImplemented::multipleRootEntities(); + } + + if (! $rsm->isSelect) { + throw FeatureNotImplemented::nonSelectStatements(); + } + + if (! ($key->cacheMode & Cache::MODE_PUT)) { + return false; + } + + $data = []; + $entityName = reset($rsm->aliasMap); + $rootAlias = key($rsm->aliasMap); + $persister = $this->uow->getEntityPersister($entityName); + + if (! $persister instanceof CachedEntityPersister) { + throw NonCacheableEntity::fromEntity($entityName); + } + + $region = $persister->getCacheRegion(); + + $cm = $this->em->getClassMetadata($entityName); + assert($cm instanceof ClassMetadata); + + foreach ($result as $index => $entity) { + $identifier = $this->uow->getEntityIdentifier($entity); + $entityKey = new EntityCacheKey($cm->rootEntityName, $identifier); + + if (($key->cacheMode & Cache::MODE_REFRESH) || ! $region->contains($entityKey)) { + // Cancel put result if entity put fail + if (! $persister->storeEntityCache($entity, $entityKey)) { + return false; + } + } + + $data[$index]['identifier'] = $identifier; + $data[$index]['associations'] = []; + + // @TODO - move to cache hydration components + foreach ($rsm->relationMap as $alias => $name) { + $parentAlias = $rsm->parentAliasMap[$alias]; + $parentClass = $rsm->aliasMap[$parentAlias]; + $metadata = $this->em->getClassMetadata($parentClass); + $assoc = $metadata->associationMappings[$name]; + $assocValue = $this->getAssociationValue($rsm, $alias, $entity); + + if ($assocValue === null) { + continue; + } + + // root entity association + if ($rootAlias === $parentAlias) { + // Cancel put result if association put fail + $assocInfo = $this->storeAssociationCache($key, $assoc, $assocValue); + if ($assocInfo === null) { + return false; + } + + $data[$index]['associations'][$name] = $assocInfo; + + continue; + } + + // store single nested association + if (! is_array($assocValue)) { + // Cancel put result if association put fail + if ($this->storeAssociationCache($key, $assoc, $assocValue) === null) { + return false; + } + + continue; + } + + // store array of nested association + foreach ($assocValue as $aVal) { + // Cancel put result if association put fail + if ($this->storeAssociationCache($key, $assoc, $aVal) === null) { + return false; + } + } + } + } + + return $this->region->put($key, new QueryCacheEntry($data)); + } + + /** + * @return mixed[]|null + * @psalm-return array{targetEntity: class-string, type: mixed, list?: array[], identifier?: array}|null + */ + private function storeAssociationCache(QueryCacheKey $key, AssociationMapping $assoc, mixed $assocValue): array|null + { + $assocPersister = $this->uow->getEntityPersister($assoc->targetEntity); + $assocMetadata = $assocPersister->getClassMetadata(); + $assocRegion = $assocPersister->getCacheRegion(); + + // Handle *-to-one associations + if ($assoc->isToOne()) { + $assocIdentifier = $this->uow->getEntityIdentifier($assocValue); + $entityKey = new EntityCacheKey($assocMetadata->rootEntityName, $assocIdentifier); + + if (! $this->uow->isUninitializedObject($assocValue) && ($key->cacheMode & Cache::MODE_REFRESH) || ! $assocRegion->contains($entityKey)) { + // Entity put fail + if (! $assocPersister->storeEntityCache($assocValue, $entityKey)) { + return null; + } + } + + return [ + 'targetEntity' => $assocMetadata->rootEntityName, + 'identifier' => $assocIdentifier, + 'type' => $assoc->type(), + ]; + } + + // Handle *-to-many associations + $list = []; + + foreach ($assocValue as $assocItemIndex => $assocItem) { + $assocIdentifier = $this->uow->getEntityIdentifier($assocItem); + $entityKey = new EntityCacheKey($assocMetadata->rootEntityName, $assocIdentifier); + + if (($key->cacheMode & Cache::MODE_REFRESH) || ! $assocRegion->contains($entityKey)) { + // Entity put fail + if (! $assocPersister->storeEntityCache($assocItem, $entityKey)) { + return null; + } + } + + $list[$assocItemIndex] = $assocIdentifier; + } + + return [ + 'targetEntity' => $assocMetadata->rootEntityName, + 'type' => $assoc->type(), + 'list' => $list, + ]; + } + + /** @psalm-return list|object|null */ + private function getAssociationValue( + ResultSetMapping $rsm, + string $assocAlias, + object $entity, + ): array|object|null { + $path = []; + $alias = $assocAlias; + + while (isset($rsm->parentAliasMap[$alias])) { + $parent = $rsm->parentAliasMap[$alias]; + $field = $rsm->relationMap[$alias]; + $class = $rsm->aliasMap[$parent]; + + array_unshift($path, [ + 'field' => $field, + 'class' => $class, + ]); + + $alias = $parent; + } + + return $this->getAssociationPathValue($entity, $path); + } + + /** + * @psalm-param array $path + * + * @psalm-return list|object|null + */ + private function getAssociationPathValue(mixed $value, array $path): array|object|null + { + $mapping = array_shift($path); + $metadata = $this->em->getClassMetadata($mapping['class']); + $assoc = $metadata->associationMappings[$mapping['field']]; + $value = $metadata->getFieldValue($value, $mapping['field']); + + if ($value === null) { + return null; + } + + if ($path === []) { + return $value; + } + + // Handle *-to-one associations + if ($assoc->isToOne()) { + return $this->getAssociationPathValue($value, $path); + } + + $values = []; + + foreach ($value as $item) { + $values[] = $this->getAssociationPathValue($item, $path); + } + + return $values; + } + + public function clear(): bool + { + return $this->region->evictAll(); + } + + public function getRegion(): Region + { + return $this->region; + } +} -- cgit v1.2.3