<?php
declare (strict_types=1);
namespace MailPoetVendor\Doctrine\ORM\Cache;
if (!defined('ABSPATH')) exit;
use MailPoetVendor\Doctrine\Common\Collections\ArrayCollection;
use MailPoetVendor\Doctrine\ORM\Cache;
use MailPoetVendor\Doctrine\ORM\Cache\Exception\FeatureNotImplemented;
use MailPoetVendor\Doctrine\ORM\Cache\Exception\NonCacheableEntity;
use MailPoetVendor\Doctrine\ORM\Cache\Logging\CacheLogger;
use MailPoetVendor\Doctrine\ORM\Cache\Persister\Entity\CachedEntityPersister;
use MailPoetVendor\Doctrine\ORM\EntityManagerInterface;
use MailPoetVendor\Doctrine\ORM\Mapping\ClassMetadata;
use MailPoetVendor\Doctrine\ORM\PersistentCollection;
use MailPoetVendor\Doctrine\ORM\Query;
use MailPoetVendor\Doctrine\ORM\Query\ResultSetMapping;
use MailPoetVendor\Doctrine\ORM\UnitOfWork;
use function array_map;
use function array_shift;
use function array_unshift;
use function assert;
use function count;
use function is_array;
use function key;
use function reset;
class DefaultQueryCache implements QueryCache
{
 private $em;
 private $uow;
 private $region;
 private $validator;
 protected $cacheLogger;
 private static $hints = [Query::HINT_CACHE_ENABLED => \true];
 public function __construct(EntityManagerInterface $em, Region $region)
 {
 $cacheConfig = $em->getConfiguration()->getSecondLevelCacheConfiguration();
 $this->em = $em;
 $this->region = $region;
 $this->uow = $em->getUnitOfWork();
 $this->cacheLogger = $cacheConfig->getCacheLogger();
 $this->validator = $cacheConfig->getQueryValidator();
 }
 public function get(QueryCacheKey $key, ResultSetMapping $rsm, array $hints = [])
 {
 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 function (array $entry) use($cm) : EntityCacheKey {
 return 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) {
 if ($this->cacheLogger !== null) {
 $this->cacheLogger->entityCacheMiss($regionName, $cacheKeys->identifiers[$index]);
 }
 return null;
 }
 if ($this->cacheLogger !== 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) {
 if ($this->cacheLogger !== 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);
 if ($this->cacheLogger !== null) {
 $this->cacheLogger->entityCacheHit($assocRegion->getName(), $assocKey);
 }
 continue;
 }
 if (!isset($assoc['list']) || empty($assoc['list'])) {
 continue;
 }
 $generateKeys = static function ($id) use($assocMetadata) : EntityCacheKey {
 return 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) {
 if ($this->cacheLogger !== 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);
 if ($this->cacheLogger !== null) {
 $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;
 }
 public function put(QueryCacheKey $key, ResultSetMapping $rsm, $result, array $hints = [])
 {
 if ($rsm->scalarMappings) {
 throw FeatureNotImplemented::scalarResults();
 }
 if (count($rsm->entityMappings) > 1) {
 throw FeatureNotImplemented::multipleRootEntities();
 }
 if (!$rsm->isSelect) {
 throw FeatureNotImplemented::nonSelectStatements();
 }
 if (($hints[Query\SqlWalker::HINT_PARTIAL] ?? \false) === \true || ($hints[Query::HINT_FORCE_PARTIAL_LOAD] ?? \false) === \true) {
 throw FeatureNotImplemented::partialEntities();
 }
 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);
 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));
 }
 private function storeAssociationCache(QueryCacheKey $key, array $assoc, $assocValue) : ?array
 {
 $assocPersister = $this->uow->getEntityPersister($assoc['targetEntity']);
 $assocMetadata = $assocPersister->getClassMetadata();
 $assocRegion = $assocPersister->getCacheRegion();
 // Handle *-to-one associations
 if ($assoc['type'] & ClassMetadata::TO_ONE) {
 $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];
 }
 private function getAssociationValue(ResultSetMapping $rsm, string $assocAlias, $entity)
 {
 $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);
 }
 private function getAssociationPathValue($value, array $path)
 {
 $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['type'] & ClassMetadata::TO_ONE) {
 return $this->getAssociationPathValue($value, $path);
 }
 $values = [];
 foreach ($value as $item) {
 $values[] = $this->getAssociationPathValue($item, $path);
 }
 return $values;
 }
 public function clear()
 {
 return $this->region->evictAll();
 }
 public function getRegion()
 {
 return $this->region;
 }
}
