Exception
9 months ago
Logging
9 months ago
Persister
9 months ago
Region
9 months ago
AssociationCacheEntry.php
9 months ago
CacheConfiguration.php
9 months ago
CacheEntry.php
9 months ago
CacheException.php
9 months ago
CacheFactory.php
9 months ago
CacheKey.php
9 months ago
CollectionCacheEntry.php
9 months ago
CollectionCacheKey.php
9 months ago
CollectionHydrator.php
9 months ago
ConcurrentRegion.php
9 months ago
DefaultCache.php
9 months ago
DefaultCacheFactory.php
9 months ago
DefaultCollectionHydrator.php
9 months ago
DefaultEntityHydrator.php
9 months ago
DefaultQueryCache.php
9 months ago
EntityCacheEntry.php
9 months ago
EntityCacheKey.php
9 months ago
EntityHydrator.php
9 months ago
Lock.php
9 months ago
LockException.php
9 months ago
MultiGetRegion.php
9 months ago
QueryCache.php
9 months ago
QueryCacheEntry.php
9 months ago
QueryCacheKey.php
9 months ago
QueryCacheValidator.php
9 months ago
Region.php
9 months ago
RegionsConfiguration.php
9 months ago
TimestampCacheEntry.php
9 months ago
TimestampCacheKey.php
9 months ago
TimestampQueryCacheValidator.php
9 months ago
TimestampRegion.php
9 months ago
index.php
9 months ago
DefaultQueryCache.php
305 lines
| 1 | <?php |
| 2 | declare (strict_types=1); |
| 3 | namespace MailPoetVendor\Doctrine\ORM\Cache; |
| 4 | if (!defined('ABSPATH')) exit; |
| 5 | use MailPoetVendor\Doctrine\Common\Collections\ArrayCollection; |
| 6 | use MailPoetVendor\Doctrine\ORM\Cache; |
| 7 | use MailPoetVendor\Doctrine\ORM\Cache\Exception\FeatureNotImplemented; |
| 8 | use MailPoetVendor\Doctrine\ORM\Cache\Exception\NonCacheableEntity; |
| 9 | use MailPoetVendor\Doctrine\ORM\Cache\Logging\CacheLogger; |
| 10 | use MailPoetVendor\Doctrine\ORM\Cache\Persister\Entity\CachedEntityPersister; |
| 11 | use MailPoetVendor\Doctrine\ORM\EntityManagerInterface; |
| 12 | use MailPoetVendor\Doctrine\ORM\Mapping\ClassMetadata; |
| 13 | use MailPoetVendor\Doctrine\ORM\PersistentCollection; |
| 14 | use MailPoetVendor\Doctrine\ORM\Query; |
| 15 | use MailPoetVendor\Doctrine\ORM\Query\ResultSetMapping; |
| 16 | use MailPoetVendor\Doctrine\ORM\UnitOfWork; |
| 17 | use function array_map; |
| 18 | use function array_shift; |
| 19 | use function array_unshift; |
| 20 | use function assert; |
| 21 | use function count; |
| 22 | use function is_array; |
| 23 | use function key; |
| 24 | use function reset; |
| 25 | class DefaultQueryCache implements QueryCache |
| 26 | { |
| 27 | private $em; |
| 28 | private $uow; |
| 29 | private $region; |
| 30 | private $validator; |
| 31 | protected $cacheLogger; |
| 32 | private static $hints = [Query::HINT_CACHE_ENABLED => \true]; |
| 33 | public function __construct(EntityManagerInterface $em, Region $region) |
| 34 | { |
| 35 | $cacheConfig = $em->getConfiguration()->getSecondLevelCacheConfiguration(); |
| 36 | $this->em = $em; |
| 37 | $this->region = $region; |
| 38 | $this->uow = $em->getUnitOfWork(); |
| 39 | $this->cacheLogger = $cacheConfig->getCacheLogger(); |
| 40 | $this->validator = $cacheConfig->getQueryValidator(); |
| 41 | } |
| 42 | public function get(QueryCacheKey $key, ResultSetMapping $rsm, array $hints = []) |
| 43 | { |
| 44 | if (!($key->cacheMode & Cache::MODE_GET)) { |
| 45 | return null; |
| 46 | } |
| 47 | $cacheEntry = $this->region->get($key); |
| 48 | if (!$cacheEntry instanceof QueryCacheEntry) { |
| 49 | return null; |
| 50 | } |
| 51 | if (!$this->validator->isValid($key, $cacheEntry)) { |
| 52 | $this->region->evict($key); |
| 53 | return null; |
| 54 | } |
| 55 | $result = []; |
| 56 | $entityName = reset($rsm->aliasMap); |
| 57 | $hasRelation = !empty($rsm->relationMap); |
| 58 | $persister = $this->uow->getEntityPersister($entityName); |
| 59 | assert($persister instanceof CachedEntityPersister); |
| 60 | $region = $persister->getCacheRegion(); |
| 61 | $regionName = $region->getName(); |
| 62 | $cm = $this->em->getClassMetadata($entityName); |
| 63 | $generateKeys = static function (array $entry) use($cm) : EntityCacheKey { |
| 64 | return new EntityCacheKey($cm->rootEntityName, $entry['identifier']); |
| 65 | }; |
| 66 | $cacheKeys = new CollectionCacheEntry(array_map($generateKeys, $cacheEntry->result)); |
| 67 | $entries = $region->getMultiple($cacheKeys) ?? []; |
| 68 | // @TODO - move to cache hydration component |
| 69 | foreach ($cacheEntry->result as $index => $entry) { |
| 70 | $entityEntry = $entries[$index] ?? null; |
| 71 | if (!$entityEntry instanceof EntityCacheEntry) { |
| 72 | if ($this->cacheLogger !== null) { |
| 73 | $this->cacheLogger->entityCacheMiss($regionName, $cacheKeys->identifiers[$index]); |
| 74 | } |
| 75 | return null; |
| 76 | } |
| 77 | if ($this->cacheLogger !== null) { |
| 78 | $this->cacheLogger->entityCacheHit($regionName, $cacheKeys->identifiers[$index]); |
| 79 | } |
| 80 | if (!$hasRelation) { |
| 81 | $result[$index] = $this->uow->createEntity($entityEntry->class, $entityEntry->resolveAssociationEntries($this->em), self::$hints); |
| 82 | continue; |
| 83 | } |
| 84 | $data = $entityEntry->data; |
| 85 | foreach ($entry['associations'] as $name => $assoc) { |
| 86 | $assocPersister = $this->uow->getEntityPersister($assoc['targetEntity']); |
| 87 | assert($assocPersister instanceof CachedEntityPersister); |
| 88 | $assocRegion = $assocPersister->getCacheRegion(); |
| 89 | $assocMetadata = $this->em->getClassMetadata($assoc['targetEntity']); |
| 90 | if ($assoc['type'] & ClassMetadata::TO_ONE) { |
| 91 | $assocKey = new EntityCacheKey($assocMetadata->rootEntityName, $assoc['identifier']); |
| 92 | $assocEntry = $assocRegion->get($assocKey); |
| 93 | if ($assocEntry === null) { |
| 94 | if ($this->cacheLogger !== null) { |
| 95 | $this->cacheLogger->entityCacheMiss($assocRegion->getName(), $assocKey); |
| 96 | } |
| 97 | $this->uow->hydrationComplete(); |
| 98 | return null; |
| 99 | } |
| 100 | $data[$name] = $this->uow->createEntity($assocEntry->class, $assocEntry->resolveAssociationEntries($this->em), self::$hints); |
| 101 | if ($this->cacheLogger !== null) { |
| 102 | $this->cacheLogger->entityCacheHit($assocRegion->getName(), $assocKey); |
| 103 | } |
| 104 | continue; |
| 105 | } |
| 106 | if (!isset($assoc['list']) || empty($assoc['list'])) { |
| 107 | continue; |
| 108 | } |
| 109 | $generateKeys = static function ($id) use($assocMetadata) : EntityCacheKey { |
| 110 | return new EntityCacheKey($assocMetadata->rootEntityName, $id); |
| 111 | }; |
| 112 | $collection = new PersistentCollection($this->em, $assocMetadata, new ArrayCollection()); |
| 113 | $assocKeys = new CollectionCacheEntry(array_map($generateKeys, $assoc['list'])); |
| 114 | $assocEntries = $assocRegion->getMultiple($assocKeys); |
| 115 | foreach ($assoc['list'] as $assocIndex => $assocId) { |
| 116 | $assocEntry = is_array($assocEntries) ? $assocEntries[$assocIndex] ?? null : null; |
| 117 | if ($assocEntry === null) { |
| 118 | if ($this->cacheLogger !== null) { |
| 119 | $this->cacheLogger->entityCacheMiss($assocRegion->getName(), $assocKeys->identifiers[$assocIndex]); |
| 120 | } |
| 121 | $this->uow->hydrationComplete(); |
| 122 | return null; |
| 123 | } |
| 124 | $element = $this->uow->createEntity($assocEntry->class, $assocEntry->resolveAssociationEntries($this->em), self::$hints); |
| 125 | $collection->hydrateSet($assocIndex, $element); |
| 126 | if ($this->cacheLogger !== null) { |
| 127 | $this->cacheLogger->entityCacheHit($assocRegion->getName(), $assocKeys->identifiers[$assocIndex]); |
| 128 | } |
| 129 | } |
| 130 | $data[$name] = $collection; |
| 131 | $collection->setInitialized(\true); |
| 132 | } |
| 133 | foreach ($data as $fieldName => $unCachedAssociationData) { |
| 134 | // In some scenarios, such as EAGER+ASSOCIATION+ID+CACHE, the |
| 135 | // cache key information in `$cacheEntry` will not contain details |
| 136 | // for fields that are associations. |
| 137 | // |
| 138 | // This means that `$data` keys for some associations that may |
| 139 | // actually not be cached will not be converted to actual association |
| 140 | // data, yet they contain L2 cache AssociationCacheEntry objects. |
| 141 | // |
| 142 | // We need to unwrap those associations into proxy references, |
| 143 | // since we don't have actual data for them except for identifiers. |
| 144 | if ($unCachedAssociationData instanceof AssociationCacheEntry) { |
| 145 | $data[$fieldName] = $this->em->getReference($unCachedAssociationData->class, $unCachedAssociationData->identifier); |
| 146 | } |
| 147 | } |
| 148 | $result[$index] = $this->uow->createEntity($entityEntry->class, $data, self::$hints); |
| 149 | } |
| 150 | $this->uow->hydrationComplete(); |
| 151 | return $result; |
| 152 | } |
| 153 | public function put(QueryCacheKey $key, ResultSetMapping $rsm, $result, array $hints = []) |
| 154 | { |
| 155 | if ($rsm->scalarMappings) { |
| 156 | throw FeatureNotImplemented::scalarResults(); |
| 157 | } |
| 158 | if (count($rsm->entityMappings) > 1) { |
| 159 | throw FeatureNotImplemented::multipleRootEntities(); |
| 160 | } |
| 161 | if (!$rsm->isSelect) { |
| 162 | throw FeatureNotImplemented::nonSelectStatements(); |
| 163 | } |
| 164 | if (($hints[Query\SqlWalker::HINT_PARTIAL] ?? \false) === \true || ($hints[Query::HINT_FORCE_PARTIAL_LOAD] ?? \false) === \true) { |
| 165 | throw FeatureNotImplemented::partialEntities(); |
| 166 | } |
| 167 | if (!($key->cacheMode & Cache::MODE_PUT)) { |
| 168 | return \false; |
| 169 | } |
| 170 | $data = []; |
| 171 | $entityName = reset($rsm->aliasMap); |
| 172 | $rootAlias = key($rsm->aliasMap); |
| 173 | $persister = $this->uow->getEntityPersister($entityName); |
| 174 | if (!$persister instanceof CachedEntityPersister) { |
| 175 | throw NonCacheableEntity::fromEntity($entityName); |
| 176 | } |
| 177 | $region = $persister->getCacheRegion(); |
| 178 | $cm = $this->em->getClassMetadata($entityName); |
| 179 | foreach ($result as $index => $entity) { |
| 180 | $identifier = $this->uow->getEntityIdentifier($entity); |
| 181 | $entityKey = new EntityCacheKey($cm->rootEntityName, $identifier); |
| 182 | if ($key->cacheMode & Cache::MODE_REFRESH || !$region->contains($entityKey)) { |
| 183 | // Cancel put result if entity put fail |
| 184 | if (!$persister->storeEntityCache($entity, $entityKey)) { |
| 185 | return \false; |
| 186 | } |
| 187 | } |
| 188 | $data[$index]['identifier'] = $identifier; |
| 189 | $data[$index]['associations'] = []; |
| 190 | // @TODO - move to cache hydration components |
| 191 | foreach ($rsm->relationMap as $alias => $name) { |
| 192 | $parentAlias = $rsm->parentAliasMap[$alias]; |
| 193 | $parentClass = $rsm->aliasMap[$parentAlias]; |
| 194 | $metadata = $this->em->getClassMetadata($parentClass); |
| 195 | $assoc = $metadata->associationMappings[$name]; |
| 196 | $assocValue = $this->getAssociationValue($rsm, $alias, $entity); |
| 197 | if ($assocValue === null) { |
| 198 | continue; |
| 199 | } |
| 200 | // root entity association |
| 201 | if ($rootAlias === $parentAlias) { |
| 202 | // Cancel put result if association put fail |
| 203 | $assocInfo = $this->storeAssociationCache($key, $assoc, $assocValue); |
| 204 | if ($assocInfo === null) { |
| 205 | return \false; |
| 206 | } |
| 207 | $data[$index]['associations'][$name] = $assocInfo; |
| 208 | continue; |
| 209 | } |
| 210 | // store single nested association |
| 211 | if (!is_array($assocValue)) { |
| 212 | // Cancel put result if association put fail |
| 213 | if ($this->storeAssociationCache($key, $assoc, $assocValue) === null) { |
| 214 | return \false; |
| 215 | } |
| 216 | continue; |
| 217 | } |
| 218 | // store array of nested association |
| 219 | foreach ($assocValue as $aVal) { |
| 220 | // Cancel put result if association put fail |
| 221 | if ($this->storeAssociationCache($key, $assoc, $aVal) === null) { |
| 222 | return \false; |
| 223 | } |
| 224 | } |
| 225 | } |
| 226 | } |
| 227 | return $this->region->put($key, new QueryCacheEntry($data)); |
| 228 | } |
| 229 | private function storeAssociationCache(QueryCacheKey $key, array $assoc, $assocValue) : ?array |
| 230 | { |
| 231 | $assocPersister = $this->uow->getEntityPersister($assoc['targetEntity']); |
| 232 | $assocMetadata = $assocPersister->getClassMetadata(); |
| 233 | $assocRegion = $assocPersister->getCacheRegion(); |
| 234 | // Handle *-to-one associations |
| 235 | if ($assoc['type'] & ClassMetadata::TO_ONE) { |
| 236 | $assocIdentifier = $this->uow->getEntityIdentifier($assocValue); |
| 237 | $entityKey = new EntityCacheKey($assocMetadata->rootEntityName, $assocIdentifier); |
| 238 | if (!$this->uow->isUninitializedObject($assocValue) && $key->cacheMode & Cache::MODE_REFRESH || !$assocRegion->contains($entityKey)) { |
| 239 | // Entity put fail |
| 240 | if (!$assocPersister->storeEntityCache($assocValue, $entityKey)) { |
| 241 | return null; |
| 242 | } |
| 243 | } |
| 244 | return ['targetEntity' => $assocMetadata->rootEntityName, 'identifier' => $assocIdentifier, 'type' => $assoc['type']]; |
| 245 | } |
| 246 | // Handle *-to-many associations |
| 247 | $list = []; |
| 248 | foreach ($assocValue as $assocItemIndex => $assocItem) { |
| 249 | $assocIdentifier = $this->uow->getEntityIdentifier($assocItem); |
| 250 | $entityKey = new EntityCacheKey($assocMetadata->rootEntityName, $assocIdentifier); |
| 251 | if ($key->cacheMode & Cache::MODE_REFRESH || !$assocRegion->contains($entityKey)) { |
| 252 | // Entity put fail |
| 253 | if (!$assocPersister->storeEntityCache($assocItem, $entityKey)) { |
| 254 | return null; |
| 255 | } |
| 256 | } |
| 257 | $list[$assocItemIndex] = $assocIdentifier; |
| 258 | } |
| 259 | return ['targetEntity' => $assocMetadata->rootEntityName, 'type' => $assoc['type'], 'list' => $list]; |
| 260 | } |
| 261 | private function getAssociationValue(ResultSetMapping $rsm, string $assocAlias, $entity) |
| 262 | { |
| 263 | $path = []; |
| 264 | $alias = $assocAlias; |
| 265 | while (isset($rsm->parentAliasMap[$alias])) { |
| 266 | $parent = $rsm->parentAliasMap[$alias]; |
| 267 | $field = $rsm->relationMap[$alias]; |
| 268 | $class = $rsm->aliasMap[$parent]; |
| 269 | array_unshift($path, ['field' => $field, 'class' => $class]); |
| 270 | $alias = $parent; |
| 271 | } |
| 272 | return $this->getAssociationPathValue($entity, $path); |
| 273 | } |
| 274 | private function getAssociationPathValue($value, array $path) |
| 275 | { |
| 276 | $mapping = array_shift($path); |
| 277 | $metadata = $this->em->getClassMetadata($mapping['class']); |
| 278 | $assoc = $metadata->associationMappings[$mapping['field']]; |
| 279 | $value = $metadata->getFieldValue($value, $mapping['field']); |
| 280 | if ($value === null) { |
| 281 | return null; |
| 282 | } |
| 283 | if ($path === []) { |
| 284 | return $value; |
| 285 | } |
| 286 | // Handle *-to-one associations |
| 287 | if ($assoc['type'] & ClassMetadata::TO_ONE) { |
| 288 | return $this->getAssociationPathValue($value, $path); |
| 289 | } |
| 290 | $values = []; |
| 291 | foreach ($value as $item) { |
| 292 | $values[] = $this->getAssociationPathValue($item, $path); |
| 293 | } |
| 294 | return $values; |
| 295 | } |
| 296 | public function clear() |
| 297 | { |
| 298 | return $this->region->evictAll(); |
| 299 | } |
| 300 | public function getRegion() |
| 301 | { |
| 302 | return $this->region; |
| 303 | } |
| 304 | } |
| 305 |