welcome back to dyb-tech
This commit is contained in:
@@ -0,0 +1,53 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Doctrine\ORM\Cache;
|
||||
|
||||
/**
|
||||
* Association cache entry
|
||||
*/
|
||||
class AssociationCacheEntry implements CacheEntry
|
||||
{
|
||||
/**
|
||||
* The entity identifier
|
||||
*
|
||||
* @readonly Public only for performance reasons, it should be considered immutable.
|
||||
* @var array<string, mixed>
|
||||
*/
|
||||
public $identifier;
|
||||
|
||||
/**
|
||||
* The entity class name
|
||||
*
|
||||
* @readonly Public only for performance reasons, it should be considered immutable.
|
||||
* @var string
|
||||
* @psalm-var class-string
|
||||
*/
|
||||
public $class;
|
||||
|
||||
/**
|
||||
* @param string $class The entity class.
|
||||
* @param array<string, mixed> $identifier The entity identifier.
|
||||
* @psalm-param class-string $class
|
||||
*/
|
||||
public function __construct($class, array $identifier)
|
||||
{
|
||||
$this->class = $class;
|
||||
$this->identifier = $identifier;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new AssociationCacheEntry
|
||||
*
|
||||
* This method allow Doctrine\Common\Cache\PhpFileCache compatibility
|
||||
*
|
||||
* @param array<string, mixed> $values array containing property values
|
||||
*
|
||||
* @return AssociationCacheEntry
|
||||
*/
|
||||
public static function __set_state(array $values)
|
||||
{
|
||||
return new self($values['class'], $values['identifier']);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,83 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Doctrine\ORM\Cache;
|
||||
|
||||
use Doctrine\ORM\Cache\Logging\CacheLogger;
|
||||
|
||||
/**
|
||||
* Configuration container for second-level cache.
|
||||
*/
|
||||
class CacheConfiguration
|
||||
{
|
||||
/** @var CacheFactory|null */
|
||||
private $cacheFactory;
|
||||
|
||||
/** @var RegionsConfiguration|null */
|
||||
private $regionsConfig;
|
||||
|
||||
/** @var CacheLogger|null */
|
||||
private $cacheLogger;
|
||||
|
||||
/** @var QueryCacheValidator|null */
|
||||
private $queryValidator;
|
||||
|
||||
/** @return CacheFactory|null */
|
||||
public function getCacheFactory()
|
||||
{
|
||||
return $this->cacheFactory;
|
||||
}
|
||||
|
||||
/** @return void */
|
||||
public function setCacheFactory(CacheFactory $factory)
|
||||
{
|
||||
$this->cacheFactory = $factory;
|
||||
}
|
||||
|
||||
/** @return CacheLogger|null */
|
||||
public function getCacheLogger()
|
||||
{
|
||||
return $this->cacheLogger;
|
||||
}
|
||||
|
||||
/** @return void */
|
||||
public function setCacheLogger(CacheLogger $logger)
|
||||
{
|
||||
$this->cacheLogger = $logger;
|
||||
}
|
||||
|
||||
/** @return RegionsConfiguration */
|
||||
public function getRegionsConfiguration()
|
||||
{
|
||||
if ($this->regionsConfig === null) {
|
||||
$this->regionsConfig = new RegionsConfiguration();
|
||||
}
|
||||
|
||||
return $this->regionsConfig;
|
||||
}
|
||||
|
||||
/** @return void */
|
||||
public function setRegionsConfiguration(RegionsConfiguration $regionsConfig)
|
||||
{
|
||||
$this->regionsConfig = $regionsConfig;
|
||||
}
|
||||
|
||||
/** @return QueryCacheValidator */
|
||||
public function getQueryValidator()
|
||||
{
|
||||
if ($this->queryValidator === null) {
|
||||
$this->queryValidator = new TimestampQueryCacheValidator(
|
||||
$this->cacheFactory->getTimestampRegion()
|
||||
);
|
||||
}
|
||||
|
||||
return $this->queryValidator;
|
||||
}
|
||||
|
||||
/** @return void */
|
||||
public function setQueryValidator(QueryCacheValidator $validator)
|
||||
{
|
||||
$this->queryValidator = $validator;
|
||||
}
|
||||
}
|
||||
+16
@@ -0,0 +1,16 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Doctrine\ORM\Cache;
|
||||
|
||||
/**
|
||||
* Cache entry interface
|
||||
*
|
||||
* <b>IMPORTANT NOTE:</b>
|
||||
*
|
||||
* Fields of classes that implement CacheEntry are public for performance reason.
|
||||
*/
|
||||
interface CacheEntry
|
||||
{
|
||||
}
|
||||
@@ -0,0 +1,61 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Doctrine\ORM\Cache;
|
||||
|
||||
use Doctrine\ORM\Exception\ORMException;
|
||||
|
||||
use function sprintf;
|
||||
|
||||
/**
|
||||
* Exception for cache.
|
||||
*/
|
||||
class CacheException extends ORMException
|
||||
{
|
||||
/**
|
||||
* @param string $sourceEntity
|
||||
* @param string $fieldName
|
||||
*
|
||||
* @return CacheException
|
||||
*/
|
||||
public static function updateReadOnlyCollection($sourceEntity, $fieldName)
|
||||
{
|
||||
return new self(sprintf('Cannot update a readonly collection "%s#%s"', $sourceEntity, $fieldName));
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated This method is not used anymore.
|
||||
*
|
||||
* @param string $entityName
|
||||
*
|
||||
* @return CacheException
|
||||
*/
|
||||
public static function updateReadOnlyEntity($entityName)
|
||||
{
|
||||
return new self(sprintf('Cannot update a readonly entity "%s"', $entityName));
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $entityName
|
||||
*
|
||||
* @return CacheException
|
||||
*/
|
||||
public static function nonCacheableEntity($entityName)
|
||||
{
|
||||
return new self(sprintf('Entity "%s" not configured as part of the second-level cache.', $entityName));
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated This method is not used anymore.
|
||||
*
|
||||
* @param string $entityName
|
||||
* @param string $field
|
||||
*
|
||||
* @return CacheException
|
||||
*/
|
||||
public static function nonCacheableEntityAssociation($entityName, $field)
|
||||
{
|
||||
return new self(sprintf('Entity association field "%s#%s" not configured as part of the second-level cache.', $entityName, $field));
|
||||
}
|
||||
}
|
||||
+89
@@ -0,0 +1,89 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Doctrine\ORM\Cache;
|
||||
|
||||
use Doctrine\ORM\Cache;
|
||||
use Doctrine\ORM\Cache\Persister\Collection\CachedCollectionPersister;
|
||||
use Doctrine\ORM\Cache\Persister\Entity\CachedEntityPersister;
|
||||
use Doctrine\ORM\EntityManagerInterface;
|
||||
use Doctrine\ORM\Mapping\ClassMetadata;
|
||||
use Doctrine\ORM\Persisters\Collection\CollectionPersister;
|
||||
use Doctrine\ORM\Persisters\Entity\EntityPersister;
|
||||
|
||||
/**
|
||||
* Contract for building second level cache regions components.
|
||||
*
|
||||
* @psalm-import-type AssociationMapping from ClassMetadata
|
||||
*/
|
||||
interface CacheFactory
|
||||
{
|
||||
/**
|
||||
* Build an entity persister for the given entity metadata.
|
||||
*
|
||||
* @param EntityManagerInterface $em The entity manager.
|
||||
* @param EntityPersister $persister The entity persister that will be cached.
|
||||
* @param ClassMetadata $metadata The entity metadata.
|
||||
*
|
||||
* @return CachedEntityPersister
|
||||
*/
|
||||
public function buildCachedEntityPersister(EntityManagerInterface $em, EntityPersister $persister, ClassMetadata $metadata);
|
||||
|
||||
/**
|
||||
* Build a collection persister for the given relation mapping.
|
||||
*
|
||||
* @param AssociationMapping $mapping The association mapping.
|
||||
*
|
||||
* @return CachedCollectionPersister
|
||||
*/
|
||||
public function buildCachedCollectionPersister(EntityManagerInterface $em, CollectionPersister $persister, array $mapping);
|
||||
|
||||
/**
|
||||
* Build a query cache based on the given region name
|
||||
*
|
||||
* @param string|null $regionName The region name.
|
||||
*
|
||||
* @return QueryCache The built query cache.
|
||||
*/
|
||||
public function buildQueryCache(EntityManagerInterface $em, $regionName = null);
|
||||
|
||||
/**
|
||||
* Build an entity hydrator
|
||||
*
|
||||
* @return EntityHydrator The built entity hydrator.
|
||||
*/
|
||||
public function buildEntityHydrator(EntityManagerInterface $em, ClassMetadata $metadata);
|
||||
|
||||
/**
|
||||
* Build a collection hydrator
|
||||
*
|
||||
* @param mixed[] $mapping The association mapping.
|
||||
*
|
||||
* @return CollectionHydrator The built collection hydrator.
|
||||
*/
|
||||
public function buildCollectionHydrator(EntityManagerInterface $em, array $mapping);
|
||||
|
||||
/**
|
||||
* Build a cache region
|
||||
*
|
||||
* @param array<string,mixed> $cache The cache configuration.
|
||||
*
|
||||
* @return Region The cache region.
|
||||
*/
|
||||
public function getRegion(array $cache);
|
||||
|
||||
/**
|
||||
* Build timestamp cache region
|
||||
*
|
||||
* @return TimestampRegion The timestamp region.
|
||||
*/
|
||||
public function getTimestampRegion();
|
||||
|
||||
/**
|
||||
* Build \Doctrine\ORM\Cache
|
||||
*
|
||||
* @return Cache
|
||||
*/
|
||||
public function createCache(EntityManagerInterface $entityManager);
|
||||
}
|
||||
+36
@@ -0,0 +1,36 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Doctrine\ORM\Cache;
|
||||
|
||||
use Doctrine\Deprecations\Deprecation;
|
||||
|
||||
/**
|
||||
* Defines entity / collection / query key to be stored in the cache region.
|
||||
* Allows multiple roles to be stored in the same cache region.
|
||||
*/
|
||||
abstract class CacheKey
|
||||
{
|
||||
/**
|
||||
* Unique identifier
|
||||
*
|
||||
* @readonly Public only for performance reasons, it should be considered immutable.
|
||||
* @var string
|
||||
*/
|
||||
public $hash;
|
||||
|
||||
public function __construct(?string $hash = null)
|
||||
{
|
||||
if ($hash === null) {
|
||||
Deprecation::trigger(
|
||||
'doctrine/orm',
|
||||
'https://github.com/doctrine/orm/pull/10212',
|
||||
'Calling %s() without providing a value for the $hash parameter is deprecated.',
|
||||
__METHOD__
|
||||
);
|
||||
} else {
|
||||
$this->hash = $hash;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Doctrine\ORM\Cache;
|
||||
|
||||
/**
|
||||
* Collection cache entry
|
||||
*/
|
||||
class CollectionCacheEntry implements CacheEntry
|
||||
{
|
||||
/**
|
||||
* The list of entity identifiers hold by the collection
|
||||
*
|
||||
* @readonly Public only for performance reasons, it should be considered immutable.
|
||||
* @var CacheKey[]
|
||||
*/
|
||||
public $identifiers;
|
||||
|
||||
/** @param CacheKey[] $identifiers List of entity identifiers hold by the collection */
|
||||
public function __construct(array $identifiers)
|
||||
{
|
||||
$this->identifiers = $identifiers;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new CollectionCacheEntry
|
||||
*
|
||||
* This method allows for Doctrine\Common\Cache\PhpFileCache compatibility
|
||||
*
|
||||
* @param array<string, mixed> $values array containing property values
|
||||
*
|
||||
* @return CollectionCacheEntry
|
||||
*/
|
||||
public static function __set_state(array $values)
|
||||
{
|
||||
return new self($values['identifiers']);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,58 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Doctrine\ORM\Cache;
|
||||
|
||||
use function implode;
|
||||
use function ksort;
|
||||
use function str_replace;
|
||||
use function strtolower;
|
||||
|
||||
/**
|
||||
* Defines entity collection roles to be stored in the cache region.
|
||||
*/
|
||||
class CollectionCacheKey extends CacheKey
|
||||
{
|
||||
/**
|
||||
* The owner entity identifier
|
||||
*
|
||||
* @readonly Public only for performance reasons, it should be considered immutable.
|
||||
* @var array<string, mixed>
|
||||
*/
|
||||
public $ownerIdentifier;
|
||||
|
||||
/**
|
||||
* The owner entity class
|
||||
*
|
||||
* @readonly Public only for performance reasons, it should be considered immutable.
|
||||
* @var string
|
||||
* @psalm-var class-string
|
||||
*/
|
||||
public $entityClass;
|
||||
|
||||
/**
|
||||
* The association name
|
||||
*
|
||||
* @readonly Public only for performance reasons, it should be considered immutable.
|
||||
* @var string
|
||||
*/
|
||||
public $association;
|
||||
|
||||
/**
|
||||
* @param string $entityClass The entity class.
|
||||
* @param string $association The field name that represents the association.
|
||||
* @param array<string, mixed> $ownerIdentifier The identifier of the owning entity.
|
||||
* @psalm-param class-string $entityClass
|
||||
*/
|
||||
public function __construct($entityClass, $association, array $ownerIdentifier)
|
||||
{
|
||||
ksort($ownerIdentifier);
|
||||
|
||||
$this->ownerIdentifier = $ownerIdentifier;
|
||||
$this->entityClass = (string) $entityClass;
|
||||
$this->association = (string) $association;
|
||||
|
||||
parent::__construct(str_replace('\\', '.', strtolower($entityClass)) . '_' . implode(' ', $ownerIdentifier) . '__' . $association);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Doctrine\ORM\Cache;
|
||||
|
||||
use Doctrine\Common\Collections\Collection;
|
||||
use Doctrine\ORM\Mapping\ClassMetadata;
|
||||
use Doctrine\ORM\PersistentCollection;
|
||||
|
||||
/**
|
||||
* Hydrator cache entry for collections
|
||||
*/
|
||||
interface CollectionHydrator
|
||||
{
|
||||
/**
|
||||
* @param array|mixed[]|Collection $collection The collection.
|
||||
*
|
||||
* @return CollectionCacheEntry
|
||||
*/
|
||||
public function buildCacheEntry(ClassMetadata $metadata, CollectionCacheKey $key, $collection);
|
||||
|
||||
/** @return mixed[]|null */
|
||||
public function loadCacheEntry(ClassMetadata $metadata, CollectionCacheKey $key, CollectionCacheEntry $entry, PersistentCollection $collection);
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Doctrine\ORM\Cache;
|
||||
|
||||
/**
|
||||
* Defines contract for concurrently managed data region.
|
||||
* It should be able to lock an specific cache entry in an atomic operation.
|
||||
*
|
||||
* When a entry is locked another process should not be able to read or write the entry.
|
||||
* All evict operation should not consider locks, even though an entry is locked evict should be able to delete the entry and its lock.
|
||||
*/
|
||||
interface ConcurrentRegion extends Region
|
||||
{
|
||||
/**
|
||||
* Attempts to read lock the mapping for the given key.
|
||||
*
|
||||
* @param CacheKey $key The key of the item to lock.
|
||||
*
|
||||
* @return Lock|null A lock instance or NULL if the lock already exists.
|
||||
*
|
||||
* @throws LockException Indicates a problem accessing the region.
|
||||
*/
|
||||
public function lock(CacheKey $key);
|
||||
|
||||
/**
|
||||
* Attempts to read unlock the mapping for the given key.
|
||||
*
|
||||
* @param CacheKey $key The key of the item to unlock.
|
||||
* @param Lock $lock The lock previously obtained from {@link readLock}
|
||||
*
|
||||
* @return bool
|
||||
*
|
||||
* @throws LockException Indicates a problem accessing the region.
|
||||
*/
|
||||
public function unlock(CacheKey $key, Lock $lock);
|
||||
}
|
||||
+308
@@ -0,0 +1,308 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Doctrine\ORM\Cache;
|
||||
|
||||
use Doctrine\ORM\Cache;
|
||||
use Doctrine\ORM\Cache\Persister\CachedPersister;
|
||||
use Doctrine\ORM\EntityManagerInterface;
|
||||
use Doctrine\ORM\Mapping\ClassMetadata;
|
||||
use Doctrine\ORM\ORMInvalidArgumentException;
|
||||
use Doctrine\ORM\Proxy\DefaultProxyClassNameResolver;
|
||||
use Doctrine\ORM\UnitOfWork;
|
||||
|
||||
use function is_array;
|
||||
use function is_object;
|
||||
|
||||
/**
|
||||
* Provides an API for querying/managing the second level cache regions.
|
||||
*/
|
||||
class DefaultCache implements Cache
|
||||
{
|
||||
/** @var EntityManagerInterface */
|
||||
private $em;
|
||||
|
||||
/** @var UnitOfWork */
|
||||
private $uow;
|
||||
|
||||
/** @var CacheFactory */
|
||||
private $cacheFactory;
|
||||
|
||||
/**
|
||||
* @var QueryCache[]
|
||||
* @psalm-var array<string, QueryCache>
|
||||
*/
|
||||
private $queryCaches = [];
|
||||
|
||||
/** @var QueryCache|null */
|
||||
private $defaultQueryCache;
|
||||
|
||||
public function __construct(EntityManagerInterface $em)
|
||||
{
|
||||
$this->em = $em;
|
||||
$this->uow = $em->getUnitOfWork();
|
||||
$this->cacheFactory = $em->getConfiguration()
|
||||
->getSecondLevelCacheConfiguration()
|
||||
->getCacheFactory();
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
public function getEntityCacheRegion($className)
|
||||
{
|
||||
$metadata = $this->em->getClassMetadata($className);
|
||||
$persister = $this->uow->getEntityPersister($metadata->rootEntityName);
|
||||
|
||||
if (! ($persister instanceof CachedPersister)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return $persister->getCacheRegion();
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
public function getCollectionCacheRegion($className, $association)
|
||||
{
|
||||
$metadata = $this->em->getClassMetadata($className);
|
||||
$persister = $this->uow->getCollectionPersister($metadata->getAssociationMapping($association));
|
||||
|
||||
if (! ($persister instanceof CachedPersister)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return $persister->getCacheRegion();
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
public function containsEntity($className, $identifier)
|
||||
{
|
||||
$metadata = $this->em->getClassMetadata($className);
|
||||
$persister = $this->uow->getEntityPersister($metadata->rootEntityName);
|
||||
|
||||
if (! ($persister instanceof CachedPersister)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return $persister->getCacheRegion()->contains($this->buildEntityCacheKey($metadata, $identifier));
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
public function evictEntity($className, $identifier)
|
||||
{
|
||||
$metadata = $this->em->getClassMetadata($className);
|
||||
$persister = $this->uow->getEntityPersister($metadata->rootEntityName);
|
||||
|
||||
if (! ($persister instanceof CachedPersister)) {
|
||||
return;
|
||||
}
|
||||
|
||||
$persister->getCacheRegion()->evict($this->buildEntityCacheKey($metadata, $identifier));
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
public function evictEntityRegion($className)
|
||||
{
|
||||
$metadata = $this->em->getClassMetadata($className);
|
||||
$persister = $this->uow->getEntityPersister($metadata->rootEntityName);
|
||||
|
||||
if (! ($persister instanceof CachedPersister)) {
|
||||
return;
|
||||
}
|
||||
|
||||
$persister->getCacheRegion()->evictAll();
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
public function evictEntityRegions()
|
||||
{
|
||||
$metadatas = $this->em->getMetadataFactory()->getAllMetadata();
|
||||
|
||||
foreach ($metadatas as $metadata) {
|
||||
$persister = $this->uow->getEntityPersister($metadata->rootEntityName);
|
||||
|
||||
if (! ($persister instanceof CachedPersister)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$persister->getCacheRegion()->evictAll();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
public function containsCollection($className, $association, $ownerIdentifier)
|
||||
{
|
||||
$metadata = $this->em->getClassMetadata($className);
|
||||
$persister = $this->uow->getCollectionPersister($metadata->getAssociationMapping($association));
|
||||
|
||||
if (! ($persister instanceof CachedPersister)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return $persister->getCacheRegion()->contains($this->buildCollectionCacheKey($metadata, $association, $ownerIdentifier));
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
public function evictCollection($className, $association, $ownerIdentifier)
|
||||
{
|
||||
$metadata = $this->em->getClassMetadata($className);
|
||||
$persister = $this->uow->getCollectionPersister($metadata->getAssociationMapping($association));
|
||||
|
||||
if (! ($persister instanceof CachedPersister)) {
|
||||
return;
|
||||
}
|
||||
|
||||
$persister->getCacheRegion()->evict($this->buildCollectionCacheKey($metadata, $association, $ownerIdentifier));
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
public function evictCollectionRegion($className, $association)
|
||||
{
|
||||
$metadata = $this->em->getClassMetadata($className);
|
||||
$persister = $this->uow->getCollectionPersister($metadata->getAssociationMapping($association));
|
||||
|
||||
if (! ($persister instanceof CachedPersister)) {
|
||||
return;
|
||||
}
|
||||
|
||||
$persister->getCacheRegion()->evictAll();
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
public function evictCollectionRegions()
|
||||
{
|
||||
$metadatas = $this->em->getMetadataFactory()->getAllMetadata();
|
||||
|
||||
foreach ($metadatas as $metadata) {
|
||||
foreach ($metadata->associationMappings as $association) {
|
||||
if (! $association['type'] & ClassMetadata::TO_MANY) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$persister = $this->uow->getCollectionPersister($association);
|
||||
|
||||
if (! ($persister instanceof CachedPersister)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$persister->getCacheRegion()->evictAll();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
public function containsQuery($regionName)
|
||||
{
|
||||
return isset($this->queryCaches[$regionName]);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
public function evictQueryRegion($regionName = null)
|
||||
{
|
||||
if ($regionName === null && $this->defaultQueryCache !== null) {
|
||||
$this->defaultQueryCache->clear();
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if (isset($this->queryCaches[$regionName])) {
|
||||
$this->queryCaches[$regionName]->clear();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
public function evictQueryRegions()
|
||||
{
|
||||
$this->getQueryCache()->clear();
|
||||
|
||||
foreach ($this->queryCaches as $queryCache) {
|
||||
$queryCache->clear();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
public function getQueryCache($regionName = null)
|
||||
{
|
||||
if ($regionName === null) {
|
||||
return $this->defaultQueryCache ?:
|
||||
$this->defaultQueryCache = $this->cacheFactory->buildQueryCache($this->em);
|
||||
}
|
||||
|
||||
if (! isset($this->queryCaches[$regionName])) {
|
||||
$this->queryCaches[$regionName] = $this->cacheFactory->buildQueryCache($this->em, $regionName);
|
||||
}
|
||||
|
||||
return $this->queryCaches[$regionName];
|
||||
}
|
||||
|
||||
/** @param mixed $identifier The entity identifier. */
|
||||
private function buildEntityCacheKey(ClassMetadata $metadata, $identifier): EntityCacheKey
|
||||
{
|
||||
if (! is_array($identifier)) {
|
||||
$identifier = $this->toIdentifierArray($metadata, $identifier);
|
||||
}
|
||||
|
||||
return new EntityCacheKey($metadata->rootEntityName, $identifier);
|
||||
}
|
||||
|
||||
/** @param mixed $ownerIdentifier The identifier of the owning entity. */
|
||||
private function buildCollectionCacheKey(
|
||||
ClassMetadata $metadata,
|
||||
string $association,
|
||||
$ownerIdentifier
|
||||
): CollectionCacheKey {
|
||||
if (! is_array($ownerIdentifier)) {
|
||||
$ownerIdentifier = $this->toIdentifierArray($metadata, $ownerIdentifier);
|
||||
}
|
||||
|
||||
return new CollectionCacheKey($metadata->rootEntityName, $association, $ownerIdentifier);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param mixed $identifier The entity identifier.
|
||||
*
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
private function toIdentifierArray(ClassMetadata $metadata, $identifier): array
|
||||
{
|
||||
if (is_object($identifier)) {
|
||||
$class = DefaultProxyClassNameResolver::getClass($identifier);
|
||||
if ($this->em->getMetadataFactory()->hasMetadataFor($class)) {
|
||||
$identifier = $this->uow->getSingleIdentifierValue($identifier);
|
||||
|
||||
if ($identifier === null) {
|
||||
throw ORMInvalidArgumentException::invalidIdentifierBindingEntity($class);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return [$metadata->identifier[0] => $identifier];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,249 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Doctrine\ORM\Cache;
|
||||
|
||||
use Doctrine\Common\Cache\Cache as LegacyCache;
|
||||
use Doctrine\Common\Cache\Psr6\CacheAdapter;
|
||||
use Doctrine\Deprecations\Deprecation;
|
||||
use Doctrine\ORM\Cache;
|
||||
use Doctrine\ORM\Cache\Persister\Collection\NonStrictReadWriteCachedCollectionPersister;
|
||||
use Doctrine\ORM\Cache\Persister\Collection\ReadOnlyCachedCollectionPersister;
|
||||
use Doctrine\ORM\Cache\Persister\Collection\ReadWriteCachedCollectionPersister;
|
||||
use Doctrine\ORM\Cache\Persister\Entity\NonStrictReadWriteCachedEntityPersister;
|
||||
use Doctrine\ORM\Cache\Persister\Entity\ReadOnlyCachedEntityPersister;
|
||||
use Doctrine\ORM\Cache\Persister\Entity\ReadWriteCachedEntityPersister;
|
||||
use Doctrine\ORM\Cache\Region\DefaultRegion;
|
||||
use Doctrine\ORM\Cache\Region\FileLockRegion;
|
||||
use Doctrine\ORM\Cache\Region\UpdateTimestampCache;
|
||||
use Doctrine\ORM\EntityManagerInterface;
|
||||
use Doctrine\ORM\Mapping\ClassMetadata;
|
||||
use Doctrine\ORM\Persisters\Collection\CollectionPersister;
|
||||
use Doctrine\ORM\Persisters\Entity\EntityPersister;
|
||||
use InvalidArgumentException;
|
||||
use LogicException;
|
||||
use Psr\Cache\CacheItemPoolInterface;
|
||||
use TypeError;
|
||||
|
||||
use function assert;
|
||||
use function get_debug_type;
|
||||
use function sprintf;
|
||||
|
||||
use const DIRECTORY_SEPARATOR;
|
||||
|
||||
class DefaultCacheFactory implements CacheFactory
|
||||
{
|
||||
/** @var CacheItemPoolInterface */
|
||||
private $cacheItemPool;
|
||||
|
||||
/** @var RegionsConfiguration */
|
||||
private $regionsConfig;
|
||||
|
||||
/** @var TimestampRegion|null */
|
||||
private $timestampRegion;
|
||||
|
||||
/** @var Region[] */
|
||||
private $regions = [];
|
||||
|
||||
/** @var string|null */
|
||||
private $fileLockRegionDirectory;
|
||||
|
||||
/** @param CacheItemPoolInterface $cacheItemPool */
|
||||
public function __construct(RegionsConfiguration $cacheConfig, $cacheItemPool)
|
||||
{
|
||||
if ($cacheItemPool instanceof LegacyCache) {
|
||||
Deprecation::trigger(
|
||||
'doctrine/orm',
|
||||
'https://github.com/doctrine/orm/pull/9322',
|
||||
'Passing an instance of %s to %s is deprecated, pass a %s instead.',
|
||||
get_debug_type($cacheItemPool),
|
||||
__METHOD__,
|
||||
CacheItemPoolInterface::class
|
||||
);
|
||||
|
||||
$this->cacheItemPool = CacheAdapter::wrap($cacheItemPool);
|
||||
} elseif (! $cacheItemPool instanceof CacheItemPoolInterface) {
|
||||
throw new TypeError(sprintf(
|
||||
'%s: Parameter #2 is expected to be an instance of %s, got %s.',
|
||||
__METHOD__,
|
||||
CacheItemPoolInterface::class,
|
||||
get_debug_type($cacheItemPool)
|
||||
));
|
||||
} else {
|
||||
$this->cacheItemPool = $cacheItemPool;
|
||||
}
|
||||
|
||||
$this->regionsConfig = $cacheConfig;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $fileLockRegionDirectory
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function setFileLockRegionDirectory($fileLockRegionDirectory)
|
||||
{
|
||||
$this->fileLockRegionDirectory = (string) $fileLockRegionDirectory;
|
||||
}
|
||||
|
||||
/** @return string */
|
||||
public function getFileLockRegionDirectory()
|
||||
{
|
||||
return $this->fileLockRegionDirectory;
|
||||
}
|
||||
|
||||
/** @return void */
|
||||
public function setRegion(Region $region)
|
||||
{
|
||||
$this->regions[$region->getName()] = $region;
|
||||
}
|
||||
|
||||
/** @return void */
|
||||
public function setTimestampRegion(TimestampRegion $region)
|
||||
{
|
||||
$this->timestampRegion = $region;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
public function buildCachedEntityPersister(EntityManagerInterface $em, EntityPersister $persister, ClassMetadata $metadata)
|
||||
{
|
||||
assert($metadata->cache !== null);
|
||||
$region = $this->getRegion($metadata->cache);
|
||||
$usage = $metadata->cache['usage'];
|
||||
|
||||
if ($usage === ClassMetadata::CACHE_USAGE_READ_ONLY) {
|
||||
return new ReadOnlyCachedEntityPersister($persister, $region, $em, $metadata);
|
||||
}
|
||||
|
||||
if ($usage === ClassMetadata::CACHE_USAGE_NONSTRICT_READ_WRITE) {
|
||||
return new NonStrictReadWriteCachedEntityPersister($persister, $region, $em, $metadata);
|
||||
}
|
||||
|
||||
if ($usage === ClassMetadata::CACHE_USAGE_READ_WRITE) {
|
||||
if (! $region instanceof ConcurrentRegion) {
|
||||
throw new InvalidArgumentException(sprintf('Unable to use access strategy type of [%s] without a ConcurrentRegion', $usage));
|
||||
}
|
||||
|
||||
return new ReadWriteCachedEntityPersister($persister, $region, $em, $metadata);
|
||||
}
|
||||
|
||||
throw new InvalidArgumentException(sprintf('Unrecognized access strategy type [%s]', $usage));
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
public function buildCachedCollectionPersister(EntityManagerInterface $em, CollectionPersister $persister, array $mapping)
|
||||
{
|
||||
assert(isset($mapping['cache']));
|
||||
$usage = $mapping['cache']['usage'];
|
||||
$region = $this->getRegion($mapping['cache']);
|
||||
|
||||
if ($usage === ClassMetadata::CACHE_USAGE_READ_ONLY) {
|
||||
return new ReadOnlyCachedCollectionPersister($persister, $region, $em, $mapping);
|
||||
}
|
||||
|
||||
if ($usage === ClassMetadata::CACHE_USAGE_NONSTRICT_READ_WRITE) {
|
||||
return new NonStrictReadWriteCachedCollectionPersister($persister, $region, $em, $mapping);
|
||||
}
|
||||
|
||||
if ($usage === ClassMetadata::CACHE_USAGE_READ_WRITE) {
|
||||
if (! $region instanceof ConcurrentRegion) {
|
||||
throw new InvalidArgumentException(sprintf('Unable to use access strategy type of [%s] without a ConcurrentRegion', $usage));
|
||||
}
|
||||
|
||||
return new ReadWriteCachedCollectionPersister($persister, $region, $em, $mapping);
|
||||
}
|
||||
|
||||
throw new InvalidArgumentException(sprintf('Unrecognized access strategy type [%s]', $usage));
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
public function buildQueryCache(EntityManagerInterface $em, $regionName = null)
|
||||
{
|
||||
return new DefaultQueryCache(
|
||||
$em,
|
||||
$this->getRegion(
|
||||
[
|
||||
'region' => $regionName ?: Cache::DEFAULT_QUERY_REGION_NAME,
|
||||
'usage' => ClassMetadata::CACHE_USAGE_NONSTRICT_READ_WRITE,
|
||||
]
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
public function buildCollectionHydrator(EntityManagerInterface $em, array $mapping)
|
||||
{
|
||||
return new DefaultCollectionHydrator($em);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
public function buildEntityHydrator(EntityManagerInterface $em, ClassMetadata $metadata)
|
||||
{
|
||||
return new DefaultEntityHydrator($em);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
public function getRegion(array $cache)
|
||||
{
|
||||
if (isset($this->regions[$cache['region']])) {
|
||||
return $this->regions[$cache['region']];
|
||||
}
|
||||
|
||||
$name = $cache['region'];
|
||||
$lifetime = $this->regionsConfig->getLifetime($cache['region']);
|
||||
$region = new DefaultRegion($name, $this->cacheItemPool, $lifetime);
|
||||
|
||||
if ($cache['usage'] === ClassMetadata::CACHE_USAGE_READ_WRITE) {
|
||||
if (
|
||||
$this->fileLockRegionDirectory === '' ||
|
||||
$this->fileLockRegionDirectory === null
|
||||
) {
|
||||
throw new LogicException(
|
||||
'If you want to use a "READ_WRITE" cache an implementation of "Doctrine\ORM\Cache\ConcurrentRegion" is required, ' .
|
||||
'The default implementation provided by doctrine is "Doctrine\ORM\Cache\Region\FileLockRegion" if you want to use it please provide a valid directory, DefaultCacheFactory#setFileLockRegionDirectory(). '
|
||||
);
|
||||
}
|
||||
|
||||
$directory = $this->fileLockRegionDirectory . DIRECTORY_SEPARATOR . $cache['region'];
|
||||
$region = new FileLockRegion($region, $directory, (string) $this->regionsConfig->getLockLifetime($cache['region']));
|
||||
}
|
||||
|
||||
return $this->regions[$cache['region']] = $region;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
public function getTimestampRegion()
|
||||
{
|
||||
if ($this->timestampRegion === null) {
|
||||
$name = Cache::DEFAULT_TIMESTAMP_REGION_NAME;
|
||||
$lifetime = $this->regionsConfig->getLifetime($name);
|
||||
|
||||
$this->timestampRegion = new UpdateTimestampCache($name, $this->cacheItemPool, $lifetime);
|
||||
}
|
||||
|
||||
return $this->timestampRegion;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
public function createCache(EntityManagerInterface $entityManager)
|
||||
{
|
||||
return new DefaultCache($entityManager);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,85 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Doctrine\ORM\Cache;
|
||||
|
||||
use Doctrine\ORM\Cache\Persister\CachedPersister;
|
||||
use Doctrine\ORM\EntityManagerInterface;
|
||||
use Doctrine\ORM\Mapping\ClassMetadata;
|
||||
use Doctrine\ORM\PersistentCollection;
|
||||
use Doctrine\ORM\Query;
|
||||
use Doctrine\ORM\UnitOfWork;
|
||||
|
||||
use function assert;
|
||||
|
||||
/**
|
||||
* Default hydrator cache for collections
|
||||
*/
|
||||
class DefaultCollectionHydrator implements CollectionHydrator
|
||||
{
|
||||
/** @var EntityManagerInterface */
|
||||
private $em;
|
||||
|
||||
/** @var UnitOfWork */
|
||||
private $uow;
|
||||
|
||||
/** @var array<string,mixed> */
|
||||
private static $hints = [Query::HINT_CACHE_ENABLED => true];
|
||||
|
||||
/** @param EntityManagerInterface $em The entity manager. */
|
||||
public function __construct(EntityManagerInterface $em)
|
||||
{
|
||||
$this->em = $em;
|
||||
$this->uow = $em->getUnitOfWork();
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
public function buildCacheEntry(ClassMetadata $metadata, CollectionCacheKey $key, $collection)
|
||||
{
|
||||
$data = [];
|
||||
|
||||
foreach ($collection as $index => $entity) {
|
||||
$data[$index] = new EntityCacheKey($metadata->rootEntityName, $this->uow->getEntityIdentifier($entity));
|
||||
}
|
||||
|
||||
return new CollectionCacheEntry($data);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
public function loadCacheEntry(ClassMetadata $metadata, CollectionCacheKey $key, CollectionCacheEntry $entry, PersistentCollection $collection)
|
||||
{
|
||||
$assoc = $metadata->associationMappings[$key->association];
|
||||
$targetPersister = $this->uow->getEntityPersister($assoc['targetEntity']);
|
||||
assert($targetPersister instanceof CachedPersister);
|
||||
$targetRegion = $targetPersister->getCacheRegion();
|
||||
$list = [];
|
||||
|
||||
/** @var EntityCacheEntry[]|null $entityEntries */
|
||||
$entityEntries = $targetRegion->getMultiple($entry);
|
||||
|
||||
if ($entityEntries === null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
foreach ($entityEntries as $index => $entityEntry) {
|
||||
$entity = $this->uow->createEntity(
|
||||
$entityEntry->class,
|
||||
$entityEntry->resolveAssociationEntries($this->em),
|
||||
self::$hints
|
||||
);
|
||||
|
||||
$collection->hydrateSet($index, $entity);
|
||||
|
||||
$list[$index] = $entity;
|
||||
}
|
||||
|
||||
$this->uow->hydrationComplete();
|
||||
|
||||
return $list;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,193 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Doctrine\ORM\Cache;
|
||||
|
||||
use Doctrine\ORM\EntityManagerInterface;
|
||||
use Doctrine\ORM\Mapping\ClassMetadata;
|
||||
use Doctrine\ORM\Proxy\DefaultProxyClassNameResolver;
|
||||
use Doctrine\ORM\Query;
|
||||
use Doctrine\ORM\UnitOfWork;
|
||||
use Doctrine\ORM\Utility\IdentifierFlattener;
|
||||
|
||||
use function array_merge;
|
||||
use function assert;
|
||||
use function is_array;
|
||||
use function is_object;
|
||||
use function reset;
|
||||
|
||||
/**
|
||||
* Default hydrator cache for entities
|
||||
*/
|
||||
class DefaultEntityHydrator implements EntityHydrator
|
||||
{
|
||||
/** @var EntityManagerInterface */
|
||||
private $em;
|
||||
|
||||
/** @var UnitOfWork */
|
||||
private $uow;
|
||||
|
||||
/**
|
||||
* The IdentifierFlattener used for manipulating identifiers
|
||||
*
|
||||
* @var IdentifierFlattener
|
||||
*/
|
||||
private $identifierFlattener;
|
||||
|
||||
/** @var array<string,mixed> */
|
||||
private static $hints = [Query::HINT_CACHE_ENABLED => true];
|
||||
|
||||
/** @param EntityManagerInterface $em The entity manager. */
|
||||
public function __construct(EntityManagerInterface $em)
|
||||
{
|
||||
$this->em = $em;
|
||||
$this->uow = $em->getUnitOfWork();
|
||||
$this->identifierFlattener = new IdentifierFlattener($em->getUnitOfWork(), $em->getMetadataFactory());
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
public function buildCacheEntry(ClassMetadata $metadata, EntityCacheKey $key, $entity)
|
||||
{
|
||||
$data = $this->uow->getOriginalEntityData($entity);
|
||||
$data = array_merge($data, $metadata->getIdentifierValues($entity)); // why update has no identifier values ?
|
||||
|
||||
if ($metadata->requiresFetchAfterChange) {
|
||||
if ($metadata->isVersioned) {
|
||||
assert($metadata->versionField !== null);
|
||||
$data[$metadata->versionField] = $metadata->getFieldValue($entity, $metadata->versionField);
|
||||
}
|
||||
|
||||
foreach ($metadata->fieldMappings as $name => $fieldMapping) {
|
||||
if (isset($fieldMapping['generated'])) {
|
||||
$data[$name] = $metadata->getFieldValue($entity, $name);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
foreach ($metadata->associationMappings as $name => $assoc) {
|
||||
if (! isset($data[$name])) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (! ($assoc['type'] & ClassMetadata::TO_ONE)) {
|
||||
unset($data[$name]);
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
if (! isset($assoc['cache'])) {
|
||||
$targetClassMetadata = $this->em->getClassMetadata($assoc['targetEntity']);
|
||||
$owningAssociation = ! $assoc['isOwningSide']
|
||||
? $targetClassMetadata->associationMappings[$assoc['mappedBy']]
|
||||
: $assoc;
|
||||
$associationIds = $this->identifierFlattener->flattenIdentifier(
|
||||
$targetClassMetadata,
|
||||
$targetClassMetadata->getIdentifierValues($data[$name])
|
||||
);
|
||||
|
||||
unset($data[$name]);
|
||||
|
||||
foreach ($associationIds as $fieldName => $fieldValue) {
|
||||
if (isset($targetClassMetadata->fieldMappings[$fieldName])) {
|
||||
$fieldMapping = $targetClassMetadata->fieldMappings[$fieldName];
|
||||
|
||||
$data[$owningAssociation['targetToSourceKeyColumns'][$fieldMapping['columnName']]] = $fieldValue;
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
$targetAssoc = $targetClassMetadata->associationMappings[$fieldName];
|
||||
|
||||
foreach ($assoc['targetToSourceKeyColumns'] as $referencedColumn => $localColumn) {
|
||||
if (isset($targetAssoc['sourceToTargetKeyColumns'][$referencedColumn])) {
|
||||
$data[$localColumn] = $fieldValue;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
if (! isset($assoc['id'])) {
|
||||
$targetClass = DefaultProxyClassNameResolver::getClass($data[$name]);
|
||||
$targetId = $this->uow->getEntityIdentifier($data[$name]);
|
||||
$data[$name] = new AssociationCacheEntry($targetClass, $targetId);
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
// handle association identifier
|
||||
$targetId = is_object($data[$name]) && $this->uow->isInIdentityMap($data[$name])
|
||||
? $this->uow->getEntityIdentifier($data[$name])
|
||||
: $data[$name];
|
||||
|
||||
// @TODO - fix it !
|
||||
// handle UnitOfWork#createEntity hash generation
|
||||
if (! is_array($targetId)) {
|
||||
$data[reset($assoc['joinColumnFieldNames'])] = $targetId;
|
||||
|
||||
$targetEntity = $this->em->getClassMetadata($assoc['targetEntity']);
|
||||
$targetId = [$targetEntity->identifier[0] => $targetId];
|
||||
}
|
||||
|
||||
$data[$name] = new AssociationCacheEntry($assoc['targetEntity'], $targetId);
|
||||
}
|
||||
|
||||
return new EntityCacheEntry($metadata->name, $data);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
public function loadCacheEntry(ClassMetadata $metadata, EntityCacheKey $key, EntityCacheEntry $entry, $entity = null)
|
||||
{
|
||||
$data = $entry->data;
|
||||
$hints = self::$hints;
|
||||
|
||||
if ($entity !== null) {
|
||||
$hints[Query::HINT_REFRESH] = true;
|
||||
$hints[Query::HINT_REFRESH_ENTITY] = $entity;
|
||||
}
|
||||
|
||||
foreach ($metadata->associationMappings as $name => $assoc) {
|
||||
if (! isset($assoc['cache']) || ! isset($data[$name])) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$assocClass = $data[$name]->class;
|
||||
$assocId = $data[$name]->identifier;
|
||||
$isEagerLoad = ($assoc['fetch'] === ClassMetadata::FETCH_EAGER || ($assoc['type'] === ClassMetadata::ONE_TO_ONE && ! $assoc['isOwningSide']));
|
||||
|
||||
if (! $isEagerLoad) {
|
||||
$data[$name] = $this->em->getReference($assocClass, $assocId);
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
$assocMetadata = $this->em->getClassMetadata($assoc['targetEntity']);
|
||||
$assocKey = new EntityCacheKey($assocMetadata->rootEntityName, $assocId);
|
||||
$assocPersister = $this->uow->getEntityPersister($assoc['targetEntity']);
|
||||
$assocRegion = $assocPersister->getCacheRegion();
|
||||
$assocEntry = $assocRegion->get($assocKey);
|
||||
|
||||
if ($assocEntry === null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$data[$name] = $this->uow->createEntity($assocEntry->class, $assocEntry->resolveAssociationEntries($this->em), $hints);
|
||||
}
|
||||
|
||||
if ($entity !== null) {
|
||||
$this->uow->registerManaged($entity, $key->identifier, $data);
|
||||
}
|
||||
|
||||
$result = $this->uow->createEntity($entry->class, $data, $hints);
|
||||
|
||||
$this->uow->hydrationComplete();
|
||||
|
||||
return $result;
|
||||
}
|
||||
}
|
||||
+466
@@ -0,0 +1,466 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Doctrine\ORM\Cache;
|
||||
|
||||
use Doctrine\Common\Collections\ArrayCollection;
|
||||
use Doctrine\ORM\Cache;
|
||||
use Doctrine\ORM\Cache\Exception\FeatureNotImplemented;
|
||||
use Doctrine\ORM\Cache\Exception\NonCacheableEntity;
|
||||
use Doctrine\ORM\Cache\Logging\CacheLogger;
|
||||
use Doctrine\ORM\Cache\Persister\Entity\CachedEntityPersister;
|
||||
use Doctrine\ORM\EntityManagerInterface;
|
||||
use Doctrine\ORM\Mapping\ClassMetadata;
|
||||
use Doctrine\ORM\PersistentCollection;
|
||||
use Doctrine\ORM\Query;
|
||||
use Doctrine\ORM\Query\ResultSetMapping;
|
||||
use 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;
|
||||
|
||||
/**
|
||||
* Default query cache implementation.
|
||||
*
|
||||
* @psalm-import-type AssociationMapping from ClassMetadata
|
||||
*/
|
||||
class DefaultQueryCache implements QueryCache
|
||||
{
|
||||
/** @var EntityManagerInterface */
|
||||
private $em;
|
||||
|
||||
/** @var UnitOfWork */
|
||||
private $uow;
|
||||
|
||||
/** @var Region */
|
||||
private $region;
|
||||
|
||||
/** @var QueryCacheValidator */
|
||||
private $validator;
|
||||
|
||||
/** @var CacheLogger|null */
|
||||
protected $cacheLogger;
|
||||
|
||||
/** @var array<string,mixed> */
|
||||
private static $hints = [Query::HINT_CACHE_ENABLED => true];
|
||||
|
||||
/**
|
||||
* @param EntityManagerInterface $em The entity manager.
|
||||
* @param Region $region The query region.
|
||||
*/
|
||||
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();
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
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;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
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);
|
||||
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));
|
||||
}
|
||||
|
||||
/**
|
||||
* @param AssociationMapping $assoc
|
||||
* @param mixed $assocValue
|
||||
*
|
||||
* @return mixed[]|null
|
||||
* @psalm-return array{targetEntity: class-string, type: mixed, list?: array[], identifier?: array}|null
|
||||
*/
|
||||
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,
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* @param object $entity
|
||||
*
|
||||
* @return mixed[]|object|null
|
||||
* @psalm-return list<mixed>|object|null
|
||||
*/
|
||||
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);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param mixed $value
|
||||
* @psalm-param array<array-key, array{field: string, class: string}> $path
|
||||
*
|
||||
* @return mixed[]|object|null
|
||||
* @psalm-return list<mixed>|object|null
|
||||
*/
|
||||
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;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
public function clear()
|
||||
{
|
||||
return $this->region->evictAll();
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
public function getRegion()
|
||||
{
|
||||
return $this->region;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,73 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Doctrine\ORM\Cache;
|
||||
|
||||
use Doctrine\ORM\EntityManagerInterface;
|
||||
|
||||
use function array_map;
|
||||
|
||||
/**
|
||||
* Entity cache entry
|
||||
*/
|
||||
class EntityCacheEntry implements CacheEntry
|
||||
{
|
||||
/**
|
||||
* The entity map data
|
||||
*
|
||||
* @readonly Public only for performance reasons, it should be considered immutable.
|
||||
* @var array<string,mixed>
|
||||
*/
|
||||
public $data;
|
||||
|
||||
/**
|
||||
* The entity class name
|
||||
*
|
||||
* @readonly Public only for performance reasons, it should be considered immutable.
|
||||
* @var string
|
||||
* @psalm-var class-string
|
||||
*/
|
||||
public $class;
|
||||
|
||||
/**
|
||||
* @param string $class The entity class.
|
||||
* @param array<string,mixed> $data The entity data.
|
||||
* @psalm-param class-string $class
|
||||
*/
|
||||
public function __construct($class, array $data)
|
||||
{
|
||||
$this->class = $class;
|
||||
$this->data = $data;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new EntityCacheEntry
|
||||
*
|
||||
* This method allow Doctrine\Common\Cache\PhpFileCache compatibility
|
||||
*
|
||||
* @param array<string,mixed> $values array containing property values
|
||||
*
|
||||
* @return EntityCacheEntry
|
||||
*/
|
||||
public static function __set_state(array $values)
|
||||
{
|
||||
return new self($values['class'], $values['data']);
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves the entity data resolving cache entries
|
||||
*
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
public function resolveAssociationEntries(EntityManagerInterface $em)
|
||||
{
|
||||
return array_map(static function ($value) use ($em) {
|
||||
if (! ($value instanceof AssociationCacheEntry)) {
|
||||
return $value;
|
||||
}
|
||||
|
||||
return $em->getReference($value->class, $value->identifier);
|
||||
}, $this->data);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Doctrine\ORM\Cache;
|
||||
|
||||
use function implode;
|
||||
use function ksort;
|
||||
use function str_replace;
|
||||
use function strtolower;
|
||||
|
||||
/**
|
||||
* Defines entity classes roles to be stored in the cache region.
|
||||
*/
|
||||
class EntityCacheKey extends CacheKey
|
||||
{
|
||||
/**
|
||||
* The entity identifier
|
||||
*
|
||||
* @readonly Public only for performance reasons, it should be considered immutable.
|
||||
* @var array<string, mixed>
|
||||
*/
|
||||
public $identifier;
|
||||
|
||||
/**
|
||||
* The entity class name
|
||||
*
|
||||
* @readonly Public only for performance reasons, it should be considered immutable.
|
||||
* @var string
|
||||
* @psalm-var class-string
|
||||
*/
|
||||
public $entityClass;
|
||||
|
||||
/**
|
||||
* @param string $entityClass The entity class name. In a inheritance hierarchy it should always be the root entity class.
|
||||
* @param array<string, mixed> $identifier The entity identifier
|
||||
* @psalm-param class-string $entityClass
|
||||
*/
|
||||
public function __construct($entityClass, array $identifier)
|
||||
{
|
||||
ksort($identifier);
|
||||
|
||||
$this->identifier = $identifier;
|
||||
$this->entityClass = $entityClass;
|
||||
|
||||
parent::__construct(str_replace('\\', '.', strtolower($entityClass) . '_' . implode(' ', $identifier)));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Doctrine\ORM\Cache;
|
||||
|
||||
use Doctrine\ORM\Mapping\ClassMetadata;
|
||||
|
||||
/**
|
||||
* Hydrator cache entry for entities
|
||||
*/
|
||||
interface EntityHydrator
|
||||
{
|
||||
/**
|
||||
* @param ClassMetadata $metadata The entity metadata.
|
||||
* @param EntityCacheKey $key The entity cache key.
|
||||
* @param object $entity The entity.
|
||||
*
|
||||
* @return EntityCacheEntry
|
||||
*/
|
||||
public function buildCacheEntry(ClassMetadata $metadata, EntityCacheKey $key, $entity);
|
||||
|
||||
/**
|
||||
* @param ClassMetadata $metadata The entity metadata.
|
||||
* @param EntityCacheKey $key The entity cache key.
|
||||
* @param EntityCacheEntry $entry The entity cache entry.
|
||||
* @param object|null $entity The entity to load the cache into. If not specified, a new entity is created.
|
||||
*
|
||||
* @return object|null
|
||||
*/
|
||||
public function loadCacheEntry(ClassMetadata $metadata, EntityCacheKey $key, EntityCacheEntry $entry, $entity = null);
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Doctrine\ORM\Cache\Exception;
|
||||
|
||||
use Doctrine\ORM\Cache\CacheException as BaseCacheException;
|
||||
|
||||
/**
|
||||
* Exception for cache.
|
||||
*/
|
||||
class CacheException extends BaseCacheException
|
||||
{
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Doctrine\ORM\Cache\Exception;
|
||||
|
||||
use function sprintf;
|
||||
|
||||
class CannotUpdateReadOnlyCollection extends CacheException
|
||||
{
|
||||
public static function fromEntityAndField(string $sourceEntity, string $fieldName): self
|
||||
{
|
||||
return new self(sprintf(
|
||||
'Cannot update a readonly collection "%s#%s"',
|
||||
$sourceEntity,
|
||||
$fieldName
|
||||
));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Doctrine\ORM\Cache\Exception;
|
||||
|
||||
use function sprintf;
|
||||
|
||||
class CannotUpdateReadOnlyEntity extends CacheException
|
||||
{
|
||||
public static function fromEntity(string $entityName): self
|
||||
{
|
||||
return new self(sprintf('Cannot update a readonly entity "%s"', $entityName));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Doctrine\ORM\Cache\Exception;
|
||||
|
||||
class FeatureNotImplemented extends CacheException
|
||||
{
|
||||
public static function scalarResults(): self
|
||||
{
|
||||
return new self('Second level cache does not support scalar results.');
|
||||
}
|
||||
|
||||
public static function multipleRootEntities(): self
|
||||
{
|
||||
return new self('Second level cache does not support multiple root entities.');
|
||||
}
|
||||
|
||||
public static function nonSelectStatements(): self
|
||||
{
|
||||
return new self('Second-level cache query supports only select statements.');
|
||||
}
|
||||
|
||||
public static function partialEntities(): self
|
||||
{
|
||||
return new self('Second level cache does not support partial entities.');
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Doctrine\ORM\Cache\Exception;
|
||||
|
||||
/** @deprecated */
|
||||
final class InvalidResultCacheDriver extends CacheException
|
||||
{
|
||||
public static function create(): self
|
||||
{
|
||||
return new self(
|
||||
'Invalid result cache driver; it must implement Doctrine\\Common\\Cache\\Cache.'
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Doctrine\ORM\Cache\Exception;
|
||||
|
||||
final class MetadataCacheNotConfigured extends CacheException
|
||||
{
|
||||
public static function create(): self
|
||||
{
|
||||
return new self('Class Metadata Cache is not configured.');
|
||||
}
|
||||
}
|
||||
+19
@@ -0,0 +1,19 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Doctrine\ORM\Cache\Exception;
|
||||
|
||||
use Doctrine\Common\Cache\Cache;
|
||||
|
||||
use function get_debug_type;
|
||||
|
||||
final class MetadataCacheUsesNonPersistentCache extends CacheException
|
||||
{
|
||||
public static function fromDriver(Cache $cache): self
|
||||
{
|
||||
return new self(
|
||||
'Metadata Cache uses a non-persistent cache driver, ' . get_debug_type($cache) . '.'
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Doctrine\ORM\Cache\Exception;
|
||||
|
||||
use function sprintf;
|
||||
|
||||
class NonCacheableEntity extends CacheException
|
||||
{
|
||||
public static function fromEntity(string $entityName): self
|
||||
{
|
||||
return new self(sprintf(
|
||||
'Entity "%s" not configured as part of the second-level cache.',
|
||||
$entityName
|
||||
));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Doctrine\ORM\Cache\Exception;
|
||||
|
||||
use function sprintf;
|
||||
|
||||
class NonCacheableEntityAssociation extends CacheException
|
||||
{
|
||||
public static function fromEntityAndField(string $entityName, string $field): self
|
||||
{
|
||||
return new self(sprintf(
|
||||
'Entity association field "%s#%s" not configured as part of the second-level cache.',
|
||||
$entityName,
|
||||
$field
|
||||
));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Doctrine\ORM\Cache\Exception;
|
||||
|
||||
final class QueryCacheNotConfigured extends CacheException
|
||||
{
|
||||
public static function create(): self
|
||||
{
|
||||
return new self('Query Cache is not configured.');
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Doctrine\ORM\Cache\Exception;
|
||||
|
||||
use Doctrine\Common\Cache\Cache;
|
||||
|
||||
use function get_debug_type;
|
||||
|
||||
final class QueryCacheUsesNonPersistentCache extends CacheException
|
||||
{
|
||||
public static function fromDriver(Cache $cache): self
|
||||
{
|
||||
return new self(
|
||||
'Query Cache uses a non-persistent cache driver, ' . get_debug_type($cache) . '.'
|
||||
);
|
||||
}
|
||||
}
|
||||
+29
@@ -0,0 +1,29 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Doctrine\ORM\Cache;
|
||||
|
||||
use function time;
|
||||
use function uniqid;
|
||||
|
||||
class Lock
|
||||
{
|
||||
/** @var string */
|
||||
public $value;
|
||||
|
||||
/** @var int */
|
||||
public $time;
|
||||
|
||||
public function __construct(string $value, ?int $time = null)
|
||||
{
|
||||
$this->value = $value;
|
||||
$this->time = $time ?: time();
|
||||
}
|
||||
|
||||
/** @return Lock */
|
||||
public static function createLockRead()
|
||||
{
|
||||
return new self(uniqid((string) time(), true));
|
||||
}
|
||||
}
|
||||
+14
@@ -0,0 +1,14 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Doctrine\ORM\Cache;
|
||||
|
||||
use Doctrine\ORM\Cache\Exception\CacheException;
|
||||
|
||||
/**
|
||||
* Lock exception for cache.
|
||||
*/
|
||||
class LockException extends CacheException
|
||||
{
|
||||
}
|
||||
@@ -0,0 +1,105 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Doctrine\ORM\Cache\Logging;
|
||||
|
||||
use Doctrine\ORM\Cache\CollectionCacheKey;
|
||||
use Doctrine\ORM\Cache\EntityCacheKey;
|
||||
use Doctrine\ORM\Cache\QueryCacheKey;
|
||||
|
||||
/**
|
||||
* Interface for logging.
|
||||
*/
|
||||
interface CacheLogger
|
||||
{
|
||||
/**
|
||||
* Log an entity put into second level cache.
|
||||
*
|
||||
* @param string $regionName The name of the cache region.
|
||||
* @param EntityCacheKey $key The cache key of the entity.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function entityCachePut($regionName, EntityCacheKey $key);
|
||||
|
||||
/**
|
||||
* Log an entity get from second level cache resulted in a hit.
|
||||
*
|
||||
* @param string $regionName The name of the cache region.
|
||||
* @param EntityCacheKey $key The cache key of the entity.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function entityCacheHit($regionName, EntityCacheKey $key);
|
||||
|
||||
/**
|
||||
* Log an entity get from second level cache resulted in a miss.
|
||||
*
|
||||
* @param string $regionName The name of the cache region.
|
||||
* @param EntityCacheKey $key The cache key of the entity.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function entityCacheMiss($regionName, EntityCacheKey $key);
|
||||
|
||||
/**
|
||||
* Log an entity put into second level cache.
|
||||
*
|
||||
* @param string $regionName The name of the cache region.
|
||||
* @param CollectionCacheKey $key The cache key of the collection.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function collectionCachePut($regionName, CollectionCacheKey $key);
|
||||
|
||||
/**
|
||||
* Log an entity get from second level cache resulted in a hit.
|
||||
*
|
||||
* @param string $regionName The name of the cache region.
|
||||
* @param CollectionCacheKey $key The cache key of the collection.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function collectionCacheHit($regionName, CollectionCacheKey $key);
|
||||
|
||||
/**
|
||||
* Log an entity get from second level cache resulted in a miss.
|
||||
*
|
||||
* @param string $regionName The name of the cache region.
|
||||
* @param CollectionCacheKey $key The cache key of the collection.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function collectionCacheMiss($regionName, CollectionCacheKey $key);
|
||||
|
||||
/**
|
||||
* Log a query put into the query cache.
|
||||
*
|
||||
* @param string $regionName The name of the cache region.
|
||||
* @param QueryCacheKey $key The cache key of the query.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function queryCachePut($regionName, QueryCacheKey $key);
|
||||
|
||||
/**
|
||||
* Log a query get from the query cache resulted in a hit.
|
||||
*
|
||||
* @param string $regionName The name of the cache region.
|
||||
* @param QueryCacheKey $key The cache key of the query.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function queryCacheHit($regionName, QueryCacheKey $key);
|
||||
|
||||
/**
|
||||
* Log a query get from the query cache resulted in a miss.
|
||||
*
|
||||
* @param string $regionName The name of the cache region.
|
||||
* @param QueryCacheKey $key The cache key of the query.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function queryCacheMiss($regionName, QueryCacheKey $key);
|
||||
}
|
||||
@@ -0,0 +1,131 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Doctrine\ORM\Cache\Logging;
|
||||
|
||||
use Doctrine\ORM\Cache\CollectionCacheKey;
|
||||
use Doctrine\ORM\Cache\EntityCacheKey;
|
||||
use Doctrine\ORM\Cache\QueryCacheKey;
|
||||
|
||||
class CacheLoggerChain implements CacheLogger
|
||||
{
|
||||
/** @var array<string, CacheLogger> */
|
||||
private $loggers = [];
|
||||
|
||||
/**
|
||||
* @param string $name
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function setLogger($name, CacheLogger $logger)
|
||||
{
|
||||
$this->loggers[$name] = $logger;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $name
|
||||
*
|
||||
* @return CacheLogger|null
|
||||
*/
|
||||
public function getLogger($name)
|
||||
{
|
||||
return $this->loggers[$name] ?? null;
|
||||
}
|
||||
|
||||
/** @return array<string, CacheLogger> */
|
||||
public function getLoggers()
|
||||
{
|
||||
return $this->loggers;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
public function collectionCacheHit($regionName, CollectionCacheKey $key)
|
||||
{
|
||||
foreach ($this->loggers as $logger) {
|
||||
$logger->collectionCacheHit($regionName, $key);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
public function collectionCacheMiss($regionName, CollectionCacheKey $key)
|
||||
{
|
||||
foreach ($this->loggers as $logger) {
|
||||
$logger->collectionCacheMiss($regionName, $key);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
public function collectionCachePut($regionName, CollectionCacheKey $key)
|
||||
{
|
||||
foreach ($this->loggers as $logger) {
|
||||
$logger->collectionCachePut($regionName, $key);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
public function entityCacheHit($regionName, EntityCacheKey $key)
|
||||
{
|
||||
foreach ($this->loggers as $logger) {
|
||||
$logger->entityCacheHit($regionName, $key);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
public function entityCacheMiss($regionName, EntityCacheKey $key)
|
||||
{
|
||||
foreach ($this->loggers as $logger) {
|
||||
$logger->entityCacheMiss($regionName, $key);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
public function entityCachePut($regionName, EntityCacheKey $key)
|
||||
{
|
||||
foreach ($this->loggers as $logger) {
|
||||
$logger->entityCachePut($regionName, $key);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
public function queryCacheHit($regionName, QueryCacheKey $key)
|
||||
{
|
||||
foreach ($this->loggers as $logger) {
|
||||
$logger->queryCacheHit($regionName, $key);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
public function queryCacheMiss($regionName, QueryCacheKey $key)
|
||||
{
|
||||
foreach ($this->loggers as $logger) {
|
||||
$logger->queryCacheMiss($regionName, $key);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
public function queryCachePut($regionName, QueryCacheKey $key)
|
||||
{
|
||||
foreach ($this->loggers as $logger) {
|
||||
$logger->queryCachePut($regionName, $key);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,217 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Doctrine\ORM\Cache\Logging;
|
||||
|
||||
use Doctrine\ORM\Cache\CollectionCacheKey;
|
||||
use Doctrine\ORM\Cache\EntityCacheKey;
|
||||
use Doctrine\ORM\Cache\QueryCacheKey;
|
||||
|
||||
use function array_sum;
|
||||
|
||||
/**
|
||||
* Provide basic second level cache statistics.
|
||||
*/
|
||||
class StatisticsCacheLogger implements CacheLogger
|
||||
{
|
||||
/** @var array<string, int> */
|
||||
private $cacheMissCountMap = [];
|
||||
|
||||
/** @var array<string, int> */
|
||||
private $cacheHitCountMap = [];
|
||||
|
||||
/** @var array<string, int> */
|
||||
private $cachePutCountMap = [];
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
public function collectionCacheMiss($regionName, CollectionCacheKey $key)
|
||||
{
|
||||
$this->cacheMissCountMap[$regionName]
|
||||
= ($this->cacheMissCountMap[$regionName] ?? 0) + 1;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
public function collectionCacheHit($regionName, CollectionCacheKey $key)
|
||||
{
|
||||
$this->cacheHitCountMap[$regionName]
|
||||
= ($this->cacheHitCountMap[$regionName] ?? 0) + 1;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
public function collectionCachePut($regionName, CollectionCacheKey $key)
|
||||
{
|
||||
$this->cachePutCountMap[$regionName]
|
||||
= ($this->cachePutCountMap[$regionName] ?? 0) + 1;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
public function entityCacheMiss($regionName, EntityCacheKey $key)
|
||||
{
|
||||
$this->cacheMissCountMap[$regionName]
|
||||
= ($this->cacheMissCountMap[$regionName] ?? 0) + 1;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
public function entityCacheHit($regionName, EntityCacheKey $key)
|
||||
{
|
||||
$this->cacheHitCountMap[$regionName]
|
||||
= ($this->cacheHitCountMap[$regionName] ?? 0) + 1;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
public function entityCachePut($regionName, EntityCacheKey $key)
|
||||
{
|
||||
$this->cachePutCountMap[$regionName]
|
||||
= ($this->cachePutCountMap[$regionName] ?? 0) + 1;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
public function queryCacheHit($regionName, QueryCacheKey $key)
|
||||
{
|
||||
$this->cacheHitCountMap[$regionName]
|
||||
= ($this->cacheHitCountMap[$regionName] ?? 0) + 1;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
public function queryCacheMiss($regionName, QueryCacheKey $key)
|
||||
{
|
||||
$this->cacheMissCountMap[$regionName]
|
||||
= ($this->cacheMissCountMap[$regionName] ?? 0) + 1;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
public function queryCachePut($regionName, QueryCacheKey $key)
|
||||
{
|
||||
$this->cachePutCountMap[$regionName]
|
||||
= ($this->cachePutCountMap[$regionName] ?? 0) + 1;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the number of entries successfully retrieved from cache.
|
||||
*
|
||||
* @param string $regionName The name of the cache region.
|
||||
*
|
||||
* @return int
|
||||
*/
|
||||
public function getRegionHitCount($regionName)
|
||||
{
|
||||
return $this->cacheHitCountMap[$regionName] ?? 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the number of cached entries *not* found in cache.
|
||||
*
|
||||
* @param string $regionName The name of the cache region.
|
||||
*
|
||||
* @return int
|
||||
*/
|
||||
public function getRegionMissCount($regionName)
|
||||
{
|
||||
return $this->cacheMissCountMap[$regionName] ?? 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the number of cacheable entries put in cache.
|
||||
*
|
||||
* @param string $regionName The name of the cache region.
|
||||
*
|
||||
* @return int
|
||||
*/
|
||||
public function getRegionPutCount($regionName)
|
||||
{
|
||||
return $this->cachePutCountMap[$regionName] ?? 0;
|
||||
}
|
||||
|
||||
/** @return array<string, int> */
|
||||
public function getRegionsMiss()
|
||||
{
|
||||
return $this->cacheMissCountMap;
|
||||
}
|
||||
|
||||
/** @return array<string, int> */
|
||||
public function getRegionsHit()
|
||||
{
|
||||
return $this->cacheHitCountMap;
|
||||
}
|
||||
|
||||
/** @return array<string, int> */
|
||||
public function getRegionsPut()
|
||||
{
|
||||
return $this->cachePutCountMap;
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear region statistics
|
||||
*
|
||||
* @param string $regionName The name of the cache region.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function clearRegionStats($regionName)
|
||||
{
|
||||
$this->cachePutCountMap[$regionName] = 0;
|
||||
$this->cacheHitCountMap[$regionName] = 0;
|
||||
$this->cacheMissCountMap[$regionName] = 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear all statistics
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function clearStats()
|
||||
{
|
||||
$this->cachePutCountMap = [];
|
||||
$this->cacheHitCountMap = [];
|
||||
$this->cacheMissCountMap = [];
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the total number of put in cache.
|
||||
*
|
||||
* @return int
|
||||
*/
|
||||
public function getPutCount()
|
||||
{
|
||||
return array_sum($this->cachePutCountMap);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the total number of entries successfully retrieved from cache.
|
||||
*
|
||||
* @return int
|
||||
*/
|
||||
public function getHitCount()
|
||||
{
|
||||
return array_sum($this->cacheHitCountMap);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the total number of cached entries *not* found in cache.
|
||||
*
|
||||
* @return int
|
||||
*/
|
||||
public function getMissCount()
|
||||
{
|
||||
return array_sum($this->cacheMissCountMap);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Doctrine\ORM\Cache;
|
||||
|
||||
/**
|
||||
* Defines a region that supports multi-get reading.
|
||||
*
|
||||
* With one method call we can get multiple items.
|
||||
*
|
||||
* @deprecated Implement {@see Region} instead.
|
||||
*/
|
||||
interface MultiGetRegion
|
||||
{
|
||||
/**
|
||||
* Get all items from the cache identified by $keys.
|
||||
* It returns NULL if some elements can not be found.
|
||||
*
|
||||
* @param CollectionCacheEntry $collection The collection of the items to be retrieved.
|
||||
*
|
||||
* @return CacheEntry[]|null The cached entries or NULL if one or more entries can not be found
|
||||
*/
|
||||
public function getMultiple(CollectionCacheEntry $collection);
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Doctrine\ORM\Cache\Persister;
|
||||
|
||||
use Doctrine\ORM\Cache\Region;
|
||||
|
||||
/**
|
||||
* Interface for persister that support second level cache.
|
||||
*/
|
||||
interface CachedPersister
|
||||
{
|
||||
/**
|
||||
* Perform whatever processing is encapsulated here after completion of the transaction.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function afterTransactionComplete();
|
||||
|
||||
/**
|
||||
* Perform whatever processing is encapsulated here after completion of the rolled-back.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function afterTransactionRolledBack();
|
||||
|
||||
/**
|
||||
* Gets the The region access.
|
||||
*
|
||||
* @return Region
|
||||
*/
|
||||
public function getCacheRegion();
|
||||
}
|
||||
+282
@@ -0,0 +1,282 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Doctrine\ORM\Cache\Persister\Collection;
|
||||
|
||||
use Doctrine\Common\Collections\Collection;
|
||||
use Doctrine\Common\Collections\Criteria;
|
||||
use Doctrine\Deprecations\Deprecation;
|
||||
use Doctrine\ORM\Cache\CollectionCacheKey;
|
||||
use Doctrine\ORM\Cache\CollectionHydrator;
|
||||
use Doctrine\ORM\Cache\EntityCacheKey;
|
||||
use Doctrine\ORM\Cache\Logging\CacheLogger;
|
||||
use Doctrine\ORM\Cache\Persister\Entity\CachedEntityPersister;
|
||||
use Doctrine\ORM\Cache\Region;
|
||||
use Doctrine\ORM\EntityManagerInterface;
|
||||
use Doctrine\ORM\Mapping\ClassMetadata;
|
||||
use Doctrine\ORM\Mapping\ClassMetadataFactory;
|
||||
use Doctrine\ORM\PersistentCollection;
|
||||
use Doctrine\ORM\Persisters\Collection\CollectionPersister;
|
||||
use Doctrine\ORM\Proxy\DefaultProxyClassNameResolver;
|
||||
use Doctrine\ORM\UnitOfWork;
|
||||
|
||||
use function array_values;
|
||||
use function assert;
|
||||
use function count;
|
||||
|
||||
/** @psalm-import-type AssociationMapping from ClassMetadata */
|
||||
abstract class AbstractCollectionPersister implements CachedCollectionPersister
|
||||
{
|
||||
/** @var UnitOfWork */
|
||||
protected $uow;
|
||||
|
||||
/** @var ClassMetadataFactory */
|
||||
protected $metadataFactory;
|
||||
|
||||
/** @var CollectionPersister */
|
||||
protected $persister;
|
||||
|
||||
/** @var ClassMetadata */
|
||||
protected $sourceEntity;
|
||||
|
||||
/** @var ClassMetadata */
|
||||
protected $targetEntity;
|
||||
|
||||
/** @var mixed[] */
|
||||
protected $association;
|
||||
|
||||
/** @var mixed[] */
|
||||
protected $queuedCache = [];
|
||||
|
||||
/** @var Region */
|
||||
protected $region;
|
||||
|
||||
/** @var string */
|
||||
protected $regionName;
|
||||
|
||||
/** @var CollectionHydrator */
|
||||
protected $hydrator;
|
||||
|
||||
/** @var CacheLogger|null */
|
||||
protected $cacheLogger;
|
||||
|
||||
/**
|
||||
* @param CollectionPersister $persister The collection persister that will be cached.
|
||||
* @param Region $region The collection region.
|
||||
* @param EntityManagerInterface $em The entity manager.
|
||||
* @param AssociationMapping $association The association mapping.
|
||||
*/
|
||||
public function __construct(CollectionPersister $persister, Region $region, EntityManagerInterface $em, array $association)
|
||||
{
|
||||
$configuration = $em->getConfiguration();
|
||||
$cacheConfig = $configuration->getSecondLevelCacheConfiguration();
|
||||
$cacheFactory = $cacheConfig->getCacheFactory();
|
||||
|
||||
$this->region = $region;
|
||||
$this->persister = $persister;
|
||||
$this->association = $association;
|
||||
$this->regionName = $region->getName();
|
||||
$this->uow = $em->getUnitOfWork();
|
||||
$this->metadataFactory = $em->getMetadataFactory();
|
||||
$this->cacheLogger = $cacheConfig->getCacheLogger();
|
||||
$this->hydrator = $cacheFactory->buildCollectionHydrator($em, $association);
|
||||
$this->sourceEntity = $em->getClassMetadata($association['sourceEntity']);
|
||||
$this->targetEntity = $em->getClassMetadata($association['targetEntity']);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
public function getCacheRegion()
|
||||
{
|
||||
return $this->region;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
public function getSourceEntityMetadata()
|
||||
{
|
||||
return $this->sourceEntity;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
public function getTargetEntityMetadata()
|
||||
{
|
||||
return $this->targetEntity;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
public function loadCollectionCache(PersistentCollection $collection, CollectionCacheKey $key)
|
||||
{
|
||||
$cache = $this->region->get($key);
|
||||
|
||||
if ($cache === null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return $this->hydrator->loadCacheEntry($this->sourceEntity, $key, $cache, $collection);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
public function storeCollectionCache(CollectionCacheKey $key, $elements)
|
||||
{
|
||||
$associationMapping = $this->sourceEntity->associationMappings[$key->association];
|
||||
$targetPersister = $this->uow->getEntityPersister($this->targetEntity->rootEntityName);
|
||||
assert($targetPersister instanceof CachedEntityPersister);
|
||||
$targetRegion = $targetPersister->getCacheRegion();
|
||||
$targetHydrator = $targetPersister->getEntityHydrator();
|
||||
|
||||
// Only preserve ordering if association configured it
|
||||
if (! (isset($associationMapping['indexBy']) && $associationMapping['indexBy'])) {
|
||||
// Elements may be an array or a Collection
|
||||
$elements = array_values($elements instanceof Collection ? $elements->getValues() : $elements);
|
||||
}
|
||||
|
||||
$entry = $this->hydrator->buildCacheEntry($this->targetEntity, $key, $elements);
|
||||
|
||||
foreach ($entry->identifiers as $index => $entityKey) {
|
||||
if ($targetRegion->contains($entityKey)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$class = $this->targetEntity;
|
||||
$className = DefaultProxyClassNameResolver::getClass($elements[$index]);
|
||||
|
||||
if ($className !== $this->targetEntity->name) {
|
||||
$class = $this->metadataFactory->getMetadataFor($className);
|
||||
}
|
||||
|
||||
$entity = $elements[$index];
|
||||
$entityEntry = $targetHydrator->buildCacheEntry($class, $entityKey, $entity);
|
||||
|
||||
$targetRegion->put($entityKey, $entityEntry);
|
||||
}
|
||||
|
||||
$cached = $this->region->put($key, $entry);
|
||||
|
||||
if ($this->cacheLogger && $cached) {
|
||||
$this->cacheLogger->collectionCachePut($this->regionName, $key);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
public function contains(PersistentCollection $collection, $element)
|
||||
{
|
||||
return $this->persister->contains($collection, $element);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
public function containsKey(PersistentCollection $collection, $key)
|
||||
{
|
||||
return $this->persister->containsKey($collection, $key);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
public function count(PersistentCollection $collection)
|
||||
{
|
||||
$ownerId = $this->uow->getEntityIdentifier($collection->getOwner());
|
||||
$key = new CollectionCacheKey($this->sourceEntity->rootEntityName, $this->association['fieldName'], $ownerId);
|
||||
$entry = $this->region->get($key);
|
||||
|
||||
if ($entry !== null) {
|
||||
return count($entry->identifiers);
|
||||
}
|
||||
|
||||
return $this->persister->count($collection);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
public function get(PersistentCollection $collection, $index)
|
||||
{
|
||||
return $this->persister->get($collection, $index);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
public function slice(PersistentCollection $collection, $offset, $length = null)
|
||||
{
|
||||
return $this->persister->slice($collection, $offset, $length);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
public function loadCriteria(PersistentCollection $collection, Criteria $criteria)
|
||||
{
|
||||
return $this->persister->loadCriteria($collection, $criteria);
|
||||
}
|
||||
|
||||
/**
|
||||
* Clears cache entries related to the current collection
|
||||
*
|
||||
* @deprecated This method is not used anymore.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
protected function evictCollectionCache(PersistentCollection $collection)
|
||||
{
|
||||
Deprecation::trigger(
|
||||
'doctrine/orm',
|
||||
'https://github.com/doctrine/orm/pull/9512',
|
||||
'The method %s() is deprecated and will be removed without replacement.'
|
||||
);
|
||||
|
||||
$key = new CollectionCacheKey(
|
||||
$this->sourceEntity->rootEntityName,
|
||||
$this->association['fieldName'],
|
||||
$this->uow->getEntityIdentifier($collection->getOwner())
|
||||
);
|
||||
|
||||
$this->region->evict($key);
|
||||
|
||||
if ($this->cacheLogger) {
|
||||
$this->cacheLogger->collectionCachePut($this->regionName, $key);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated This method is not used anymore.
|
||||
*
|
||||
* @param string $targetEntity
|
||||
* @param object $element
|
||||
* @psalm-param class-string $targetEntity
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
protected function evictElementCache($targetEntity, $element)
|
||||
{
|
||||
Deprecation::trigger(
|
||||
'doctrine/orm',
|
||||
'https://github.com/doctrine/orm/pull/9512',
|
||||
'The method %s() is deprecated and will be removed without replacement.'
|
||||
);
|
||||
|
||||
$targetPersister = $this->uow->getEntityPersister($targetEntity);
|
||||
assert($targetPersister instanceof CachedEntityPersister);
|
||||
$targetRegion = $targetPersister->getCacheRegion();
|
||||
$key = new EntityCacheKey($targetEntity, $this->uow->getEntityIdentifier($element));
|
||||
|
||||
$targetRegion->evict($key);
|
||||
|
||||
if ($this->cacheLogger) {
|
||||
$this->cacheLogger->entityCachePut($targetRegion->getName(), $key);
|
||||
}
|
||||
}
|
||||
}
|
||||
+40
@@ -0,0 +1,40 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Doctrine\ORM\Cache\Persister\Collection;
|
||||
|
||||
use Doctrine\Common\Collections\Collection;
|
||||
use Doctrine\ORM\Cache\CollectionCacheKey;
|
||||
use Doctrine\ORM\Cache\Persister\CachedPersister;
|
||||
use Doctrine\ORM\Mapping\ClassMetadata;
|
||||
use Doctrine\ORM\PersistentCollection;
|
||||
use Doctrine\ORM\Persisters\Collection\CollectionPersister;
|
||||
|
||||
/**
|
||||
* Interface for second level cache collection persisters.
|
||||
*/
|
||||
interface CachedCollectionPersister extends CachedPersister, CollectionPersister
|
||||
{
|
||||
/** @return ClassMetadata */
|
||||
public function getSourceEntityMetadata();
|
||||
|
||||
/** @return ClassMetadata */
|
||||
public function getTargetEntityMetadata();
|
||||
|
||||
/**
|
||||
* Loads a collection from cache
|
||||
*
|
||||
* @return mixed[]|null
|
||||
*/
|
||||
public function loadCollectionCache(PersistentCollection $collection, CollectionCacheKey $key);
|
||||
|
||||
/**
|
||||
* Stores a collection into cache
|
||||
*
|
||||
* @param mixed[]|Collection $elements
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function storeCollectionCache(CollectionCacheKey $key, $elements);
|
||||
}
|
||||
Vendored
+86
@@ -0,0 +1,86 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Doctrine\ORM\Cache\Persister\Collection;
|
||||
|
||||
use Doctrine\ORM\Cache\CollectionCacheKey;
|
||||
use Doctrine\ORM\PersistentCollection;
|
||||
|
||||
use function spl_object_id;
|
||||
|
||||
class NonStrictReadWriteCachedCollectionPersister extends AbstractCollectionPersister
|
||||
{
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
public function afterTransactionComplete()
|
||||
{
|
||||
if (isset($this->queuedCache['update'])) {
|
||||
foreach ($this->queuedCache['update'] as $item) {
|
||||
$this->storeCollectionCache($item['key'], $item['list']);
|
||||
}
|
||||
}
|
||||
|
||||
if (isset($this->queuedCache['delete'])) {
|
||||
foreach ($this->queuedCache['delete'] as $key) {
|
||||
$this->region->evict($key);
|
||||
}
|
||||
}
|
||||
|
||||
$this->queuedCache = [];
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
public function afterTransactionRolledBack()
|
||||
{
|
||||
$this->queuedCache = [];
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
public function delete(PersistentCollection $collection)
|
||||
{
|
||||
$ownerId = $this->uow->getEntityIdentifier($collection->getOwner());
|
||||
$key = new CollectionCacheKey($this->sourceEntity->rootEntityName, $this->association['fieldName'], $ownerId);
|
||||
|
||||
$this->persister->delete($collection);
|
||||
|
||||
$this->queuedCache['delete'][spl_object_id($collection)] = $key;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
public function update(PersistentCollection $collection)
|
||||
{
|
||||
$isInitialized = $collection->isInitialized();
|
||||
$isDirty = $collection->isDirty();
|
||||
|
||||
if (! $isInitialized && ! $isDirty) {
|
||||
return;
|
||||
}
|
||||
|
||||
$ownerId = $this->uow->getEntityIdentifier($collection->getOwner());
|
||||
$key = new CollectionCacheKey($this->sourceEntity->rootEntityName, $this->association['fieldName'], $ownerId);
|
||||
|
||||
// Invalidate non initialized collections OR ordered collection
|
||||
if ($isDirty && ! $isInitialized || isset($this->association['orderBy'])) {
|
||||
$this->persister->update($collection);
|
||||
|
||||
$this->queuedCache['delete'][spl_object_id($collection)] = $key;
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
$this->persister->update($collection);
|
||||
|
||||
$this->queuedCache['update'][spl_object_id($collection)] = [
|
||||
'key' => $key,
|
||||
'list' => $collection,
|
||||
];
|
||||
}
|
||||
}
|
||||
+27
@@ -0,0 +1,27 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Doctrine\ORM\Cache\Persister\Collection;
|
||||
|
||||
use Doctrine\ORM\Cache\Exception\CannotUpdateReadOnlyCollection;
|
||||
use Doctrine\ORM\PersistentCollection;
|
||||
use Doctrine\ORM\Proxy\DefaultProxyClassNameResolver;
|
||||
|
||||
class ReadOnlyCachedCollectionPersister extends NonStrictReadWriteCachedCollectionPersister
|
||||
{
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
public function update(PersistentCollection $collection)
|
||||
{
|
||||
if ($collection->isDirty() && $collection->getSnapshot()) {
|
||||
throw CannotUpdateReadOnlyCollection::fromEntityAndField(
|
||||
DefaultProxyClassNameResolver::getClass($collection->getOwner()),
|
||||
$this->association['fieldName']
|
||||
);
|
||||
}
|
||||
|
||||
parent::update($collection);
|
||||
}
|
||||
}
|
||||
+113
@@ -0,0 +1,113 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Doctrine\ORM\Cache\Persister\Collection;
|
||||
|
||||
use Doctrine\ORM\Cache\CollectionCacheKey;
|
||||
use Doctrine\ORM\Cache\ConcurrentRegion;
|
||||
use Doctrine\ORM\EntityManagerInterface;
|
||||
use Doctrine\ORM\Mapping\ClassMetadata;
|
||||
use Doctrine\ORM\PersistentCollection;
|
||||
use Doctrine\ORM\Persisters\Collection\CollectionPersister;
|
||||
|
||||
use function spl_object_id;
|
||||
|
||||
/** @psalm-import-type AssociationMapping from ClassMetadata */
|
||||
class ReadWriteCachedCollectionPersister extends AbstractCollectionPersister
|
||||
{
|
||||
/** @param AssociationMapping $association The association mapping. */
|
||||
public function __construct(CollectionPersister $persister, ConcurrentRegion $region, EntityManagerInterface $em, array $association)
|
||||
{
|
||||
parent::__construct($persister, $region, $em, $association);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
public function afterTransactionComplete()
|
||||
{
|
||||
if (isset($this->queuedCache['update'])) {
|
||||
foreach ($this->queuedCache['update'] as $item) {
|
||||
$this->region->evict($item['key']);
|
||||
}
|
||||
}
|
||||
|
||||
if (isset($this->queuedCache['delete'])) {
|
||||
foreach ($this->queuedCache['delete'] as $item) {
|
||||
$this->region->evict($item['key']);
|
||||
}
|
||||
}
|
||||
|
||||
$this->queuedCache = [];
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
public function afterTransactionRolledBack()
|
||||
{
|
||||
if (isset($this->queuedCache['update'])) {
|
||||
foreach ($this->queuedCache['update'] as $item) {
|
||||
$this->region->evict($item['key']);
|
||||
}
|
||||
}
|
||||
|
||||
if (isset($this->queuedCache['delete'])) {
|
||||
foreach ($this->queuedCache['delete'] as $item) {
|
||||
$this->region->evict($item['key']);
|
||||
}
|
||||
}
|
||||
|
||||
$this->queuedCache = [];
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
public function delete(PersistentCollection $collection)
|
||||
{
|
||||
$ownerId = $this->uow->getEntityIdentifier($collection->getOwner());
|
||||
$key = new CollectionCacheKey($this->sourceEntity->rootEntityName, $this->association['fieldName'], $ownerId);
|
||||
$lock = $this->region->lock($key);
|
||||
|
||||
$this->persister->delete($collection);
|
||||
|
||||
if ($lock === null) {
|
||||
return;
|
||||
}
|
||||
|
||||
$this->queuedCache['delete'][spl_object_id($collection)] = [
|
||||
'key' => $key,
|
||||
'lock' => $lock,
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
public function update(PersistentCollection $collection)
|
||||
{
|
||||
$isInitialized = $collection->isInitialized();
|
||||
$isDirty = $collection->isDirty();
|
||||
|
||||
if (! $isInitialized && ! $isDirty) {
|
||||
return;
|
||||
}
|
||||
|
||||
$this->persister->update($collection);
|
||||
|
||||
$ownerId = $this->uow->getEntityIdentifier($collection->getOwner());
|
||||
$key = new CollectionCacheKey($this->sourceEntity->rootEntityName, $this->association['fieldName'], $ownerId);
|
||||
$lock = $this->region->lock($key);
|
||||
|
||||
if ($lock === null) {
|
||||
return;
|
||||
}
|
||||
|
||||
$this->queuedCache['update'][spl_object_id($collection)] = [
|
||||
'key' => $key,
|
||||
'lock' => $lock,
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,619 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Doctrine\ORM\Cache\Persister\Entity;
|
||||
|
||||
use Doctrine\Common\Collections\Criteria;
|
||||
use Doctrine\ORM\Cache;
|
||||
use Doctrine\ORM\Cache\CollectionCacheKey;
|
||||
use Doctrine\ORM\Cache\EntityCacheKey;
|
||||
use Doctrine\ORM\Cache\EntityHydrator;
|
||||
use Doctrine\ORM\Cache\Logging\CacheLogger;
|
||||
use Doctrine\ORM\Cache\Persister\CachedPersister;
|
||||
use Doctrine\ORM\Cache\QueryCacheKey;
|
||||
use Doctrine\ORM\Cache\Region;
|
||||
use Doctrine\ORM\Cache\TimestampCacheKey;
|
||||
use Doctrine\ORM\Cache\TimestampRegion;
|
||||
use Doctrine\ORM\EntityManagerInterface;
|
||||
use Doctrine\ORM\Mapping\ClassMetadata;
|
||||
use Doctrine\ORM\Mapping\ClassMetadataFactory;
|
||||
use Doctrine\ORM\PersistentCollection;
|
||||
use Doctrine\ORM\Persisters\Entity\EntityPersister;
|
||||
use Doctrine\ORM\Proxy\DefaultProxyClassNameResolver;
|
||||
use Doctrine\ORM\UnitOfWork;
|
||||
|
||||
use function array_merge;
|
||||
use function assert;
|
||||
use function serialize;
|
||||
use function sha1;
|
||||
|
||||
abstract class AbstractEntityPersister implements CachedEntityPersister
|
||||
{
|
||||
/** @var UnitOfWork */
|
||||
protected $uow;
|
||||
|
||||
/** @var ClassMetadataFactory */
|
||||
protected $metadataFactory;
|
||||
|
||||
/** @var EntityPersister */
|
||||
protected $persister;
|
||||
|
||||
/** @var ClassMetadata */
|
||||
protected $class;
|
||||
|
||||
/** @var mixed[] */
|
||||
protected $queuedCache = [];
|
||||
|
||||
/** @var Region */
|
||||
protected $region;
|
||||
|
||||
/** @var TimestampRegion */
|
||||
protected $timestampRegion;
|
||||
|
||||
/** @var TimestampCacheKey */
|
||||
protected $timestampKey;
|
||||
|
||||
/** @var EntityHydrator */
|
||||
protected $hydrator;
|
||||
|
||||
/** @var Cache */
|
||||
protected $cache;
|
||||
|
||||
/** @var CacheLogger|null */
|
||||
protected $cacheLogger;
|
||||
|
||||
/** @var string */
|
||||
protected $regionName;
|
||||
|
||||
/**
|
||||
* Associations configured as FETCH_EAGER, as well as all inverse one-to-one associations.
|
||||
*
|
||||
* @var array<string>|null
|
||||
*/
|
||||
protected $joinedAssociations;
|
||||
|
||||
/**
|
||||
* @param EntityPersister $persister The entity persister to cache.
|
||||
* @param Region $region The entity cache region.
|
||||
* @param EntityManagerInterface $em The entity manager.
|
||||
* @param ClassMetadata $class The entity metadata.
|
||||
*/
|
||||
public function __construct(EntityPersister $persister, Region $region, EntityManagerInterface $em, ClassMetadata $class)
|
||||
{
|
||||
$configuration = $em->getConfiguration();
|
||||
$cacheConfig = $configuration->getSecondLevelCacheConfiguration();
|
||||
$cacheFactory = $cacheConfig->getCacheFactory();
|
||||
|
||||
$this->class = $class;
|
||||
$this->region = $region;
|
||||
$this->persister = $persister;
|
||||
$this->cache = $em->getCache();
|
||||
$this->regionName = $region->getName();
|
||||
$this->uow = $em->getUnitOfWork();
|
||||
$this->metadataFactory = $em->getMetadataFactory();
|
||||
$this->cacheLogger = $cacheConfig->getCacheLogger();
|
||||
$this->timestampRegion = $cacheFactory->getTimestampRegion();
|
||||
$this->hydrator = $cacheFactory->buildEntityHydrator($em, $class);
|
||||
$this->timestampKey = new TimestampCacheKey($this->class->rootEntityName);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
public function addInsert($entity)
|
||||
{
|
||||
$this->persister->addInsert($entity);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
public function getInserts()
|
||||
{
|
||||
return $this->persister->getInserts();
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
public function getSelectSQL($criteria, $assoc = null, $lockMode = null, $limit = null, $offset = null, ?array $orderBy = null)
|
||||
{
|
||||
return $this->persister->getSelectSQL($criteria, $assoc, $lockMode, $limit, $offset, $orderBy);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
public function getCountSQL($criteria = [])
|
||||
{
|
||||
return $this->persister->getCountSQL($criteria);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
public function getInsertSQL()
|
||||
{
|
||||
return $this->persister->getInsertSQL();
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
public function getResultSetMapping()
|
||||
{
|
||||
return $this->persister->getResultSetMapping();
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
public function getSelectConditionStatementSQL($field, $value, $assoc = null, $comparison = null)
|
||||
{
|
||||
return $this->persister->getSelectConditionStatementSQL($field, $value, $assoc, $comparison);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
public function exists($entity, ?Criteria $extraConditions = null)
|
||||
{
|
||||
if ($extraConditions === null) {
|
||||
$key = new EntityCacheKey($this->class->rootEntityName, $this->class->getIdentifierValues($entity));
|
||||
|
||||
if ($this->region->contains($key)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return $this->persister->exists($entity, $extraConditions);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
public function getCacheRegion()
|
||||
{
|
||||
return $this->region;
|
||||
}
|
||||
|
||||
/** @return EntityHydrator */
|
||||
public function getEntityHydrator()
|
||||
{
|
||||
return $this->hydrator;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
public function storeEntityCache($entity, EntityCacheKey $key)
|
||||
{
|
||||
$class = $this->class;
|
||||
$className = DefaultProxyClassNameResolver::getClass($entity);
|
||||
|
||||
if ($className !== $this->class->name) {
|
||||
$class = $this->metadataFactory->getMetadataFor($className);
|
||||
}
|
||||
|
||||
$entry = $this->hydrator->buildCacheEntry($class, $key, $entity);
|
||||
$cached = $this->region->put($key, $entry);
|
||||
|
||||
if ($this->cacheLogger && $cached) {
|
||||
$this->cacheLogger->entityCachePut($this->regionName, $key);
|
||||
}
|
||||
|
||||
return $cached;
|
||||
}
|
||||
|
||||
/** @param object $entity */
|
||||
private function storeJoinedAssociations($entity): void
|
||||
{
|
||||
if ($this->joinedAssociations === null) {
|
||||
$associations = [];
|
||||
|
||||
foreach ($this->class->associationMappings as $name => $assoc) {
|
||||
if (
|
||||
isset($assoc['cache']) &&
|
||||
($assoc['type'] & ClassMetadata::TO_ONE) &&
|
||||
($assoc['fetch'] === ClassMetadata::FETCH_EAGER || ! $assoc['isOwningSide'])
|
||||
) {
|
||||
$associations[] = $name;
|
||||
}
|
||||
}
|
||||
|
||||
$this->joinedAssociations = $associations;
|
||||
}
|
||||
|
||||
foreach ($this->joinedAssociations as $name) {
|
||||
$assoc = $this->class->associationMappings[$name];
|
||||
$assocEntity = $this->class->getFieldValue($entity, $name);
|
||||
|
||||
if ($assocEntity === null) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$assocId = $this->uow->getEntityIdentifier($assocEntity);
|
||||
$assocMetadata = $this->metadataFactory->getMetadataFor($assoc['targetEntity']);
|
||||
$assocKey = new EntityCacheKey($assocMetadata->rootEntityName, $assocId);
|
||||
$assocPersister = $this->uow->getEntityPersister($assoc['targetEntity']);
|
||||
|
||||
$assocPersister->storeEntityCache($assocEntity, $assocKey);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates a string of currently query
|
||||
*
|
||||
* @param string $query
|
||||
* @param string[]|Criteria $criteria
|
||||
* @param string[]|null $orderBy
|
||||
* @param int|null $limit
|
||||
* @param int|null $offset
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
protected function getHash($query, $criteria, ?array $orderBy = null, $limit = null, $offset = null)
|
||||
{
|
||||
[$params] = $criteria instanceof Criteria
|
||||
? $this->persister->expandCriteriaParameters($criteria)
|
||||
: $this->persister->expandParameters($criteria);
|
||||
|
||||
return sha1($query . serialize($params) . serialize($orderBy) . $limit . $offset);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
public function expandParameters($criteria)
|
||||
{
|
||||
return $this->persister->expandParameters($criteria);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
public function expandCriteriaParameters(Criteria $criteria)
|
||||
{
|
||||
return $this->persister->expandCriteriaParameters($criteria);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
public function getClassMetadata()
|
||||
{
|
||||
return $this->persister->getClassMetadata();
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
public function getManyToManyCollection(array $assoc, $sourceEntity, $offset = null, $limit = null)
|
||||
{
|
||||
return $this->persister->getManyToManyCollection($assoc, $sourceEntity, $offset, $limit);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
public function getOneToManyCollection(array $assoc, $sourceEntity, $offset = null, $limit = null)
|
||||
{
|
||||
return $this->persister->getOneToManyCollection($assoc, $sourceEntity, $offset, $limit);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
public function getOwningTable($fieldName)
|
||||
{
|
||||
return $this->persister->getOwningTable($fieldName);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
public function executeInserts()
|
||||
{
|
||||
// The commit order/foreign key relationships may make it necessary that multiple calls to executeInsert()
|
||||
// are performed, so collect all the new entities.
|
||||
$newInserts = $this->persister->getInserts();
|
||||
|
||||
if ($newInserts) {
|
||||
$this->queuedCache['insert'] = array_merge($this->queuedCache['insert'] ?? [], $newInserts);
|
||||
}
|
||||
|
||||
return $this->persister->executeInserts();
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
public function load(array $criteria, $entity = null, $assoc = null, array $hints = [], $lockMode = null, $limit = null, ?array $orderBy = null)
|
||||
{
|
||||
if ($entity !== null || $assoc !== null || $hints !== [] || $lockMode !== null) {
|
||||
return $this->persister->load($criteria, $entity, $assoc, $hints, $lockMode, $limit, $orderBy);
|
||||
}
|
||||
|
||||
//handle only EntityRepository#findOneBy
|
||||
$query = $this->persister->getSelectSQL($criteria, null, null, $limit, null, $orderBy);
|
||||
$hash = $this->getHash($query, $criteria, null, null, null);
|
||||
$rsm = $this->getResultSetMapping();
|
||||
$queryKey = new QueryCacheKey($hash, 0, Cache::MODE_NORMAL, $this->timestampKey);
|
||||
$queryCache = $this->cache->getQueryCache($this->regionName);
|
||||
$result = $queryCache->get($queryKey, $rsm);
|
||||
|
||||
if ($result !== null) {
|
||||
if ($this->cacheLogger) {
|
||||
$this->cacheLogger->queryCacheHit($this->regionName, $queryKey);
|
||||
}
|
||||
|
||||
return $result[0];
|
||||
}
|
||||
|
||||
$result = $this->persister->load($criteria, $entity, $assoc, $hints, $lockMode, $limit, $orderBy);
|
||||
|
||||
if ($result === null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$cached = $queryCache->put($queryKey, $rsm, [$result]);
|
||||
|
||||
if ($this->cacheLogger) {
|
||||
$this->cacheLogger->queryCacheMiss($this->regionName, $queryKey);
|
||||
|
||||
if ($cached) {
|
||||
$this->cacheLogger->queryCachePut($this->regionName, $queryKey);
|
||||
}
|
||||
}
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
public function loadAll(array $criteria = [], ?array $orderBy = null, $limit = null, $offset = null)
|
||||
{
|
||||
$query = $this->persister->getSelectSQL($criteria, null, null, $limit, $offset, $orderBy);
|
||||
$hash = $this->getHash($query, $criteria, null, null, null);
|
||||
$rsm = $this->getResultSetMapping();
|
||||
$queryKey = new QueryCacheKey($hash, 0, Cache::MODE_NORMAL, $this->timestampKey);
|
||||
$queryCache = $this->cache->getQueryCache($this->regionName);
|
||||
$result = $queryCache->get($queryKey, $rsm);
|
||||
|
||||
if ($result !== null) {
|
||||
if ($this->cacheLogger) {
|
||||
$this->cacheLogger->queryCacheHit($this->regionName, $queryKey);
|
||||
}
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
$result = $this->persister->loadAll($criteria, $orderBy, $limit, $offset);
|
||||
$cached = $queryCache->put($queryKey, $rsm, $result);
|
||||
|
||||
if ($this->cacheLogger) {
|
||||
if ($result) {
|
||||
$this->cacheLogger->queryCacheMiss($this->regionName, $queryKey);
|
||||
}
|
||||
|
||||
if ($cached) {
|
||||
$this->cacheLogger->queryCachePut($this->regionName, $queryKey);
|
||||
}
|
||||
}
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
public function loadById(array $identifier, $entity = null)
|
||||
{
|
||||
$cacheKey = new EntityCacheKey($this->class->rootEntityName, $identifier);
|
||||
$cacheEntry = $this->region->get($cacheKey);
|
||||
$class = $this->class;
|
||||
|
||||
if ($cacheEntry !== null) {
|
||||
if ($cacheEntry->class !== $this->class->name) {
|
||||
$class = $this->metadataFactory->getMetadataFor($cacheEntry->class);
|
||||
}
|
||||
|
||||
$cachedEntity = $this->hydrator->loadCacheEntry($class, $cacheKey, $cacheEntry, $entity);
|
||||
|
||||
if ($cachedEntity !== null) {
|
||||
if ($this->cacheLogger) {
|
||||
$this->cacheLogger->entityCacheHit($this->regionName, $cacheKey);
|
||||
}
|
||||
|
||||
return $cachedEntity;
|
||||
}
|
||||
}
|
||||
|
||||
$entity = $this->persister->loadById($identifier, $entity);
|
||||
|
||||
if ($entity === null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$class = $this->class;
|
||||
$className = DefaultProxyClassNameResolver::getClass($entity);
|
||||
|
||||
if ($className !== $this->class->name) {
|
||||
$class = $this->metadataFactory->getMetadataFor($className);
|
||||
}
|
||||
|
||||
$cacheEntry = $this->hydrator->buildCacheEntry($class, $cacheKey, $entity);
|
||||
$cached = $this->region->put($cacheKey, $cacheEntry);
|
||||
|
||||
if ($cached && ($this->joinedAssociations === null || $this->joinedAssociations)) {
|
||||
$this->storeJoinedAssociations($entity);
|
||||
}
|
||||
|
||||
if ($this->cacheLogger) {
|
||||
if ($cached) {
|
||||
$this->cacheLogger->entityCachePut($this->regionName, $cacheKey);
|
||||
}
|
||||
|
||||
$this->cacheLogger->entityCacheMiss($this->regionName, $cacheKey);
|
||||
}
|
||||
|
||||
return $entity;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
public function count($criteria = [])
|
||||
{
|
||||
return $this->persister->count($criteria);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
public function loadCriteria(Criteria $criteria)
|
||||
{
|
||||
$orderBy = $criteria->getOrderings();
|
||||
$limit = $criteria->getMaxResults();
|
||||
$offset = $criteria->getFirstResult();
|
||||
$query = $this->persister->getSelectSQL($criteria);
|
||||
$hash = $this->getHash($query, $criteria, $orderBy, $limit, $offset);
|
||||
$rsm = $this->getResultSetMapping();
|
||||
$queryKey = new QueryCacheKey($hash, 0, Cache::MODE_NORMAL, $this->timestampKey);
|
||||
$queryCache = $this->cache->getQueryCache($this->regionName);
|
||||
$cacheResult = $queryCache->get($queryKey, $rsm);
|
||||
|
||||
if ($cacheResult !== null) {
|
||||
if ($this->cacheLogger) {
|
||||
$this->cacheLogger->queryCacheHit($this->regionName, $queryKey);
|
||||
}
|
||||
|
||||
return $cacheResult;
|
||||
}
|
||||
|
||||
$result = $this->persister->loadCriteria($criteria);
|
||||
$cached = $queryCache->put($queryKey, $rsm, $result);
|
||||
|
||||
if ($this->cacheLogger) {
|
||||
if ($result) {
|
||||
$this->cacheLogger->queryCacheMiss($this->regionName, $queryKey);
|
||||
}
|
||||
|
||||
if ($cached) {
|
||||
$this->cacheLogger->queryCachePut($this->regionName, $queryKey);
|
||||
}
|
||||
}
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
public function loadManyToManyCollection(array $assoc, $sourceEntity, PersistentCollection $collection)
|
||||
{
|
||||
$persister = $this->uow->getCollectionPersister($assoc);
|
||||
$hasCache = ($persister instanceof CachedPersister);
|
||||
|
||||
if (! $hasCache) {
|
||||
return $this->persister->loadManyToManyCollection($assoc, $sourceEntity, $collection);
|
||||
}
|
||||
|
||||
$ownerId = $this->uow->getEntityIdentifier($collection->getOwner());
|
||||
$key = $this->buildCollectionCacheKey($assoc, $ownerId);
|
||||
$list = $persister->loadCollectionCache($collection, $key);
|
||||
|
||||
if ($list !== null) {
|
||||
if ($this->cacheLogger) {
|
||||
$this->cacheLogger->collectionCacheHit($persister->getCacheRegion()->getName(), $key);
|
||||
}
|
||||
|
||||
return $list;
|
||||
}
|
||||
|
||||
$list = $this->persister->loadManyToManyCollection($assoc, $sourceEntity, $collection);
|
||||
|
||||
$persister->storeCollectionCache($key, $list);
|
||||
|
||||
if ($this->cacheLogger) {
|
||||
$this->cacheLogger->collectionCacheMiss($persister->getCacheRegion()->getName(), $key);
|
||||
}
|
||||
|
||||
return $list;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
public function loadOneToManyCollection(array $assoc, $sourceEntity, PersistentCollection $collection)
|
||||
{
|
||||
$persister = $this->uow->getCollectionPersister($assoc);
|
||||
$hasCache = ($persister instanceof CachedPersister);
|
||||
|
||||
if (! $hasCache) {
|
||||
return $this->persister->loadOneToManyCollection($assoc, $sourceEntity, $collection);
|
||||
}
|
||||
|
||||
$ownerId = $this->uow->getEntityIdentifier($collection->getOwner());
|
||||
$key = $this->buildCollectionCacheKey($assoc, $ownerId);
|
||||
$list = $persister->loadCollectionCache($collection, $key);
|
||||
|
||||
if ($list !== null) {
|
||||
if ($this->cacheLogger) {
|
||||
$this->cacheLogger->collectionCacheHit($persister->getCacheRegion()->getName(), $key);
|
||||
}
|
||||
|
||||
return $list;
|
||||
}
|
||||
|
||||
$list = $this->persister->loadOneToManyCollection($assoc, $sourceEntity, $collection);
|
||||
|
||||
$persister->storeCollectionCache($key, $list);
|
||||
|
||||
if ($this->cacheLogger) {
|
||||
$this->cacheLogger->collectionCacheMiss($persister->getCacheRegion()->getName(), $key);
|
||||
}
|
||||
|
||||
return $list;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
public function loadOneToOneEntity(array $assoc, $sourceEntity, array $identifier = [])
|
||||
{
|
||||
return $this->persister->loadOneToOneEntity($assoc, $sourceEntity, $identifier);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
public function lock(array $criteria, $lockMode)
|
||||
{
|
||||
$this->persister->lock($criteria, $lockMode);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
public function refresh(array $id, $entity, $lockMode = null)
|
||||
{
|
||||
$this->persister->refresh($id, $entity, $lockMode);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string, mixed> $association
|
||||
* @param array<string, mixed> $ownerId
|
||||
*
|
||||
* @return CollectionCacheKey
|
||||
*/
|
||||
protected function buildCollectionCacheKey(array $association, $ownerId)
|
||||
{
|
||||
$metadata = $this->metadataFactory->getMetadataFor($association['sourceEntity']);
|
||||
assert($metadata instanceof ClassMetadata);
|
||||
|
||||
return new CollectionCacheKey($metadata->rootEntityName, $association['fieldName'], $ownerId);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Doctrine\ORM\Cache\Persister\Entity;
|
||||
|
||||
use Doctrine\ORM\Cache\EntityCacheKey;
|
||||
use Doctrine\ORM\Cache\EntityHydrator;
|
||||
use Doctrine\ORM\Cache\Persister\CachedPersister;
|
||||
use Doctrine\ORM\Persisters\Entity\EntityPersister;
|
||||
|
||||
/**
|
||||
* Interface for second level cache entity persisters.
|
||||
*/
|
||||
interface CachedEntityPersister extends CachedPersister, EntityPersister
|
||||
{
|
||||
/** @return EntityHydrator */
|
||||
public function getEntityHydrator();
|
||||
|
||||
/**
|
||||
* @param object $entity
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function storeEntityCache($entity, EntityCacheKey $key);
|
||||
}
|
||||
+100
@@ -0,0 +1,100 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Doctrine\ORM\Cache\Persister\Entity;
|
||||
|
||||
use Doctrine\ORM\Cache\EntityCacheKey;
|
||||
|
||||
use function get_class;
|
||||
|
||||
/**
|
||||
* Specific non-strict read/write cached entity persister
|
||||
*/
|
||||
class NonStrictReadWriteCachedEntityPersister extends AbstractEntityPersister
|
||||
{
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
public function afterTransactionComplete()
|
||||
{
|
||||
$isChanged = false;
|
||||
|
||||
if (isset($this->queuedCache['insert'])) {
|
||||
foreach ($this->queuedCache['insert'] as $entity) {
|
||||
$isChanged = $this->updateCache($entity, $isChanged);
|
||||
}
|
||||
}
|
||||
|
||||
if (isset($this->queuedCache['update'])) {
|
||||
foreach ($this->queuedCache['update'] as $entity) {
|
||||
$isChanged = $this->updateCache($entity, $isChanged);
|
||||
}
|
||||
}
|
||||
|
||||
if (isset($this->queuedCache['delete'])) {
|
||||
foreach ($this->queuedCache['delete'] as $key) {
|
||||
$this->region->evict($key);
|
||||
|
||||
$isChanged = true;
|
||||
}
|
||||
}
|
||||
|
||||
if ($isChanged) {
|
||||
$this->timestampRegion->update($this->timestampKey);
|
||||
}
|
||||
|
||||
$this->queuedCache = [];
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
public function afterTransactionRolledBack()
|
||||
{
|
||||
$this->queuedCache = [];
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
public function delete($entity)
|
||||
{
|
||||
$key = new EntityCacheKey($this->class->rootEntityName, $this->uow->getEntityIdentifier($entity));
|
||||
$deleted = $this->persister->delete($entity);
|
||||
|
||||
if ($deleted) {
|
||||
$this->region->evict($key);
|
||||
}
|
||||
|
||||
$this->queuedCache['delete'][] = $key;
|
||||
|
||||
return $deleted;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
public function update($entity)
|
||||
{
|
||||
$this->persister->update($entity);
|
||||
|
||||
$this->queuedCache['update'][] = $entity;
|
||||
}
|
||||
|
||||
/** @param object $entity */
|
||||
private function updateCache($entity, bool $isChanged): bool
|
||||
{
|
||||
$class = $this->metadataFactory->getMetadataFor(get_class($entity));
|
||||
$key = new EntityCacheKey($class->rootEntityName, $this->uow->getEntityIdentifier($entity));
|
||||
$entry = $this->hydrator->buildCacheEntry($class, $key, $entity);
|
||||
$cached = $this->region->put($key, $entry);
|
||||
$isChanged = $isChanged || $cached;
|
||||
|
||||
if ($this->cacheLogger && $cached) {
|
||||
$this->cacheLogger->entityCachePut($this->regionName, $key);
|
||||
}
|
||||
|
||||
return $isChanged;
|
||||
}
|
||||
}
|
||||
+22
@@ -0,0 +1,22 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Doctrine\ORM\Cache\Persister\Entity;
|
||||
|
||||
use Doctrine\ORM\Cache\Exception\CannotUpdateReadOnlyEntity;
|
||||
use Doctrine\ORM\Proxy\DefaultProxyClassNameResolver;
|
||||
|
||||
/**
|
||||
* Specific read-only region entity persister
|
||||
*/
|
||||
class ReadOnlyCachedEntityPersister extends NonStrictReadWriteCachedEntityPersister
|
||||
{
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
public function update($entity)
|
||||
{
|
||||
throw CannotUpdateReadOnlyEntity::fromEntity(DefaultProxyClassNameResolver::getClass($entity));
|
||||
}
|
||||
}
|
||||
+117
@@ -0,0 +1,117 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Doctrine\ORM\Cache\Persister\Entity;
|
||||
|
||||
use Doctrine\ORM\Cache\ConcurrentRegion;
|
||||
use Doctrine\ORM\Cache\EntityCacheKey;
|
||||
use Doctrine\ORM\EntityManagerInterface;
|
||||
use Doctrine\ORM\Mapping\ClassMetadata;
|
||||
use Doctrine\ORM\Persisters\Entity\EntityPersister;
|
||||
|
||||
/**
|
||||
* Specific read-write entity persister
|
||||
*/
|
||||
class ReadWriteCachedEntityPersister extends AbstractEntityPersister
|
||||
{
|
||||
public function __construct(EntityPersister $persister, ConcurrentRegion $region, EntityManagerInterface $em, ClassMetadata $class)
|
||||
{
|
||||
parent::__construct($persister, $region, $em, $class);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
public function afterTransactionComplete()
|
||||
{
|
||||
$isChanged = true;
|
||||
|
||||
if (isset($this->queuedCache['update'])) {
|
||||
foreach ($this->queuedCache['update'] as $item) {
|
||||
$this->region->evict($item['key']);
|
||||
|
||||
$isChanged = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (isset($this->queuedCache['delete'])) {
|
||||
foreach ($this->queuedCache['delete'] as $item) {
|
||||
$this->region->evict($item['key']);
|
||||
|
||||
$isChanged = true;
|
||||
}
|
||||
}
|
||||
|
||||
if ($isChanged) {
|
||||
$this->timestampRegion->update($this->timestampKey);
|
||||
}
|
||||
|
||||
$this->queuedCache = [];
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
public function afterTransactionRolledBack()
|
||||
{
|
||||
if (isset($this->queuedCache['update'])) {
|
||||
foreach ($this->queuedCache['update'] as $item) {
|
||||
$this->region->evict($item['key']);
|
||||
}
|
||||
}
|
||||
|
||||
if (isset($this->queuedCache['delete'])) {
|
||||
foreach ($this->queuedCache['delete'] as $item) {
|
||||
$this->region->evict($item['key']);
|
||||
}
|
||||
}
|
||||
|
||||
$this->queuedCache = [];
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
public function delete($entity)
|
||||
{
|
||||
$key = new EntityCacheKey($this->class->rootEntityName, $this->uow->getEntityIdentifier($entity));
|
||||
$lock = $this->region->lock($key);
|
||||
$deleted = $this->persister->delete($entity);
|
||||
|
||||
if ($deleted) {
|
||||
$this->region->evict($key);
|
||||
}
|
||||
|
||||
if ($lock === null) {
|
||||
return $deleted;
|
||||
}
|
||||
|
||||
$this->queuedCache['delete'][] = [
|
||||
'lock' => $lock,
|
||||
'key' => $key,
|
||||
];
|
||||
|
||||
return $deleted;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
public function update($entity)
|
||||
{
|
||||
$key = new EntityCacheKey($this->class->rootEntityName, $this->uow->getEntityIdentifier($entity));
|
||||
$lock = $this->region->lock($key);
|
||||
|
||||
$this->persister->update($entity);
|
||||
|
||||
if ($lock === null) {
|
||||
return;
|
||||
}
|
||||
|
||||
$this->queuedCache['update'][] = [
|
||||
'lock' => $lock,
|
||||
'key' => $key,
|
||||
];
|
||||
}
|
||||
}
|
||||
+35
@@ -0,0 +1,35 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Doctrine\ORM\Cache;
|
||||
|
||||
use Doctrine\ORM\Query\ResultSetMapping;
|
||||
|
||||
/**
|
||||
* Defines the contract for caches capable of storing query results.
|
||||
* These caches should only concern themselves with storing the matching result ids.
|
||||
*/
|
||||
interface QueryCache
|
||||
{
|
||||
/** @return bool */
|
||||
public function clear();
|
||||
|
||||
/**
|
||||
* @param mixed $result
|
||||
* @param mixed[] $hints
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function put(QueryCacheKey $key, ResultSetMapping $rsm, $result, array $hints = []);
|
||||
|
||||
/**
|
||||
* @param mixed[] $hints
|
||||
*
|
||||
* @return mixed[]|null
|
||||
*/
|
||||
public function get(QueryCacheKey $key, ResultSetMapping $rsm, array $hints = []);
|
||||
|
||||
/** @return Region */
|
||||
public function getRegion();
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Doctrine\ORM\Cache;
|
||||
|
||||
use function microtime;
|
||||
|
||||
/**
|
||||
* Query cache entry
|
||||
*/
|
||||
class QueryCacheEntry implements CacheEntry
|
||||
{
|
||||
/**
|
||||
* List of entity identifiers
|
||||
*
|
||||
* @readonly Public only for performance reasons, it should be considered immutable.
|
||||
* @var array<string, mixed>
|
||||
*/
|
||||
public $result;
|
||||
|
||||
/**
|
||||
* Time creation of this cache entry
|
||||
*
|
||||
* @readonly Public only for performance reasons, it should be considered immutable.
|
||||
* @var float
|
||||
*/
|
||||
public $time;
|
||||
|
||||
/**
|
||||
* @param array<string, mixed> $result
|
||||
* @param float|null $time
|
||||
*/
|
||||
public function __construct($result, $time = null)
|
||||
{
|
||||
$this->result = $result;
|
||||
$this->time = $time ?: microtime(true);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string, mixed> $values
|
||||
*
|
||||
* @return QueryCacheEntry
|
||||
*/
|
||||
public static function __set_state(array $values)
|
||||
{
|
||||
return new self($values['result'], $values['time']);
|
||||
}
|
||||
}
|
||||
+50
@@ -0,0 +1,50 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Doctrine\ORM\Cache;
|
||||
|
||||
use Doctrine\ORM\Cache;
|
||||
|
||||
/**
|
||||
* A cache key that identifies a particular query.
|
||||
*/
|
||||
class QueryCacheKey extends CacheKey
|
||||
{
|
||||
/**
|
||||
* Cache key lifetime
|
||||
*
|
||||
* @readonly Public only for performance reasons, it should be considered immutable.
|
||||
* @var int
|
||||
*/
|
||||
public $lifetime;
|
||||
|
||||
/**
|
||||
* Cache mode
|
||||
*
|
||||
* @readonly Public only for performance reasons, it should be considered immutable.
|
||||
* @var int
|
||||
* @psalm-var Cache::MODE_*
|
||||
*/
|
||||
public $cacheMode;
|
||||
|
||||
/**
|
||||
* @readonly Public only for performance reasons, it should be considered immutable.
|
||||
* @var TimestampCacheKey|null
|
||||
*/
|
||||
public $timestampKey;
|
||||
|
||||
/** @psalm-param Cache::MODE_* $cacheMode */
|
||||
public function __construct(
|
||||
string $cacheId,
|
||||
int $lifetime = 0,
|
||||
int $cacheMode = Cache::MODE_NORMAL,
|
||||
?TimestampCacheKey $timestampKey = null
|
||||
) {
|
||||
$this->lifetime = $lifetime;
|
||||
$this->cacheMode = $cacheMode;
|
||||
$this->timestampKey = $timestampKey;
|
||||
|
||||
parent::__construct($cacheId);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Doctrine\ORM\Cache;
|
||||
|
||||
/**
|
||||
* Cache query validator interface.
|
||||
*/
|
||||
interface QueryCacheValidator
|
||||
{
|
||||
/**
|
||||
* Checks if the query entry is valid
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function isValid(QueryCacheKey $key, QueryCacheEntry $entry);
|
||||
}
|
||||
+73
@@ -0,0 +1,73 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Doctrine\ORM\Cache;
|
||||
|
||||
use Doctrine\ORM\Cache\Exception\CacheException;
|
||||
|
||||
/**
|
||||
* Defines a contract for accessing a particular named region.
|
||||
*/
|
||||
interface Region extends MultiGetRegion
|
||||
{
|
||||
/**
|
||||
* Retrieve the name of this region.
|
||||
*
|
||||
* @return string The region name
|
||||
*/
|
||||
public function getName();
|
||||
|
||||
/**
|
||||
* Determine whether this region contains data for the given key.
|
||||
*
|
||||
* @param CacheKey $key The cache key
|
||||
*
|
||||
* @return bool TRUE if the underlying cache contains corresponding data; FALSE otherwise.
|
||||
*/
|
||||
public function contains(CacheKey $key);
|
||||
|
||||
/**
|
||||
* Get an item from the cache.
|
||||
*
|
||||
* @param CacheKey $key The key of the item to be retrieved.
|
||||
*
|
||||
* @return CacheEntry|null The cached entry or NULL
|
||||
*
|
||||
* @throws CacheException Indicates a problem accessing the item or region.
|
||||
*/
|
||||
public function get(CacheKey $key);
|
||||
|
||||
/**
|
||||
* Put an item into the cache.
|
||||
*
|
||||
* @param CacheKey $key The key under which to cache the item.
|
||||
* @param CacheEntry $entry The entry to cache.
|
||||
* @param Lock|null $lock The lock previously obtained.
|
||||
*
|
||||
* @return bool
|
||||
*
|
||||
* @throws CacheException Indicates a problem accessing the region.
|
||||
*/
|
||||
public function put(CacheKey $key, CacheEntry $entry, ?Lock $lock = null);
|
||||
|
||||
/**
|
||||
* Remove an item from the cache.
|
||||
*
|
||||
* @param CacheKey $key The key under which to cache the item.
|
||||
*
|
||||
* @return bool
|
||||
*
|
||||
* @throws CacheException Indicates a problem accessing the region.
|
||||
*/
|
||||
public function evict(CacheKey $key);
|
||||
|
||||
/**
|
||||
* Remove all contents of this particular cache region.
|
||||
*
|
||||
* @return bool
|
||||
*
|
||||
* @throws CacheException Indicates problem accessing the region.
|
||||
*/
|
||||
public function evictAll();
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Doctrine\ORM\Cache\Region;
|
||||
|
||||
/**
|
||||
* A cache region that enables the retrieval of multiple elements with one call
|
||||
*
|
||||
* @deprecated Use {@link DefaultRegion} instead.
|
||||
*/
|
||||
class DefaultMultiGetRegion extends DefaultRegion
|
||||
{
|
||||
}
|
||||
@@ -0,0 +1,213 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Doctrine\ORM\Cache\Region;
|
||||
|
||||
use Closure;
|
||||
use Doctrine\Common\Cache\Cache as LegacyCache;
|
||||
use Doctrine\Common\Cache\CacheProvider;
|
||||
use Doctrine\Common\Cache\Psr6\CacheAdapter;
|
||||
use Doctrine\Common\Cache\Psr6\DoctrineProvider;
|
||||
use Doctrine\Deprecations\Deprecation;
|
||||
use Doctrine\ORM\Cache\CacheEntry;
|
||||
use Doctrine\ORM\Cache\CacheKey;
|
||||
use Doctrine\ORM\Cache\CollectionCacheEntry;
|
||||
use Doctrine\ORM\Cache\Lock;
|
||||
use Doctrine\ORM\Cache\Region;
|
||||
use Psr\Cache\CacheItemInterface;
|
||||
use Psr\Cache\CacheItemPoolInterface;
|
||||
use Traversable;
|
||||
use TypeError;
|
||||
|
||||
use function array_map;
|
||||
use function get_debug_type;
|
||||
use function iterator_to_array;
|
||||
use function sprintf;
|
||||
use function strtr;
|
||||
|
||||
/**
|
||||
* The simplest cache region compatible with all doctrine-cache drivers.
|
||||
*/
|
||||
class DefaultRegion implements Region
|
||||
{
|
||||
/** @internal since 2.11, this constant will be private in 3.0. */
|
||||
public const REGION_KEY_SEPARATOR = '_';
|
||||
private const REGION_PREFIX = 'DC2_REGION_';
|
||||
|
||||
/**
|
||||
* @deprecated since 2.11, this property will be removed in 3.0.
|
||||
*
|
||||
* @var LegacyCache
|
||||
*/
|
||||
protected $cache;
|
||||
|
||||
/**
|
||||
* @internal since 2.11, this property will be private in 3.0.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $name;
|
||||
|
||||
/**
|
||||
* @internal since 2.11, this property will be private in 3.0.
|
||||
*
|
||||
* @var int
|
||||
*/
|
||||
protected $lifetime = 0;
|
||||
|
||||
/** @var CacheItemPoolInterface */
|
||||
private $cacheItemPool;
|
||||
|
||||
/** @param CacheItemPoolInterface $cacheItemPool */
|
||||
public function __construct(string $name, $cacheItemPool, int $lifetime = 0)
|
||||
{
|
||||
if ($cacheItemPool instanceof LegacyCache) {
|
||||
Deprecation::trigger(
|
||||
'doctrine/orm',
|
||||
'https://github.com/doctrine/orm/pull/9322',
|
||||
'Passing an instance of %s to %s is deprecated, pass a %s instead.',
|
||||
get_debug_type($cacheItemPool),
|
||||
__METHOD__,
|
||||
CacheItemPoolInterface::class
|
||||
);
|
||||
|
||||
$this->cache = $cacheItemPool;
|
||||
$this->cacheItemPool = CacheAdapter::wrap($cacheItemPool);
|
||||
} elseif (! $cacheItemPool instanceof CacheItemPoolInterface) {
|
||||
throw new TypeError(sprintf(
|
||||
'%s: Parameter #2 is expected to be an instance of %s, got %s.',
|
||||
__METHOD__,
|
||||
CacheItemPoolInterface::class,
|
||||
get_debug_type($cacheItemPool)
|
||||
));
|
||||
} else {
|
||||
$this->cache = DoctrineProvider::wrap($cacheItemPool);
|
||||
$this->cacheItemPool = $cacheItemPool;
|
||||
}
|
||||
|
||||
$this->name = $name;
|
||||
$this->lifetime = $lifetime;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
public function getName()
|
||||
{
|
||||
return $this->name;
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated
|
||||
*
|
||||
* @return CacheProvider
|
||||
*/
|
||||
public function getCache()
|
||||
{
|
||||
return $this->cache;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
public function contains(CacheKey $key)
|
||||
{
|
||||
return $this->cacheItemPool->hasItem($this->getCacheEntryKey($key));
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
public function get(CacheKey $key)
|
||||
{
|
||||
$item = $this->cacheItemPool->getItem($this->getCacheEntryKey($key));
|
||||
$entry = $item->isHit() ? $item->get() : null;
|
||||
|
||||
if (! $entry instanceof CacheEntry) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return $entry;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
public function getMultiple(CollectionCacheEntry $collection)
|
||||
{
|
||||
$keys = array_map(
|
||||
Closure::fromCallable([$this, 'getCacheEntryKey']),
|
||||
$collection->identifiers
|
||||
);
|
||||
/** @var iterable<string, CacheItemInterface> $items */
|
||||
$items = $this->cacheItemPool->getItems($keys);
|
||||
if ($items instanceof Traversable) {
|
||||
$items = iterator_to_array($items);
|
||||
}
|
||||
|
||||
$result = [];
|
||||
foreach ($keys as $arrayKey => $cacheKey) {
|
||||
if (! isset($items[$cacheKey]) || ! $items[$cacheKey]->isHit()) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$entry = $items[$cacheKey]->get();
|
||||
if (! $entry instanceof CacheEntry) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$result[$arrayKey] = $entry;
|
||||
}
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function put(CacheKey $key, CacheEntry $entry, ?Lock $lock = null)
|
||||
{
|
||||
$item = $this->cacheItemPool
|
||||
->getItem($this->getCacheEntryKey($key))
|
||||
->set($entry);
|
||||
|
||||
if ($this->lifetime > 0) {
|
||||
$item->expiresAfter($this->lifetime);
|
||||
}
|
||||
|
||||
return $this->cacheItemPool->save($item);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function evict(CacheKey $key)
|
||||
{
|
||||
return $this->cacheItemPool->deleteItem($this->getCacheEntryKey($key));
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function evictAll()
|
||||
{
|
||||
return $this->cacheItemPool->clear(self::REGION_PREFIX . $this->name);
|
||||
}
|
||||
|
||||
/**
|
||||
* @internal since 2.11, this method will be private in 3.0.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
protected function getCacheEntryKey(CacheKey $key)
|
||||
{
|
||||
return self::REGION_PREFIX . $this->name . self::REGION_KEY_SEPARATOR . strtr($key->hash, '{}()/\@:', '________');
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,236 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Doctrine\ORM\Cache\Region;
|
||||
|
||||
use Doctrine\ORM\Cache\CacheEntry;
|
||||
use Doctrine\ORM\Cache\CacheKey;
|
||||
use Doctrine\ORM\Cache\CollectionCacheEntry;
|
||||
use Doctrine\ORM\Cache\ConcurrentRegion;
|
||||
use Doctrine\ORM\Cache\Lock;
|
||||
use Doctrine\ORM\Cache\Region;
|
||||
use InvalidArgumentException;
|
||||
|
||||
use function array_filter;
|
||||
use function array_map;
|
||||
use function chmod;
|
||||
use function file_get_contents;
|
||||
use function file_put_contents;
|
||||
use function fileatime;
|
||||
use function glob;
|
||||
use function is_dir;
|
||||
use function is_file;
|
||||
use function is_writable;
|
||||
use function mkdir;
|
||||
use function sprintf;
|
||||
use function time;
|
||||
use function unlink;
|
||||
|
||||
use const DIRECTORY_SEPARATOR;
|
||||
use const LOCK_EX;
|
||||
|
||||
/**
|
||||
* Very naive concurrent region, based on file locks.
|
||||
*/
|
||||
class FileLockRegion implements ConcurrentRegion
|
||||
{
|
||||
public const LOCK_EXTENSION = 'lock';
|
||||
|
||||
/** @var Region */
|
||||
private $region;
|
||||
|
||||
/** @var string */
|
||||
private $directory;
|
||||
|
||||
/** @psalm-var numeric-string */
|
||||
private $lockLifetime;
|
||||
|
||||
/**
|
||||
* @param string $directory
|
||||
* @param numeric-string $lockLifetime
|
||||
*
|
||||
* @throws InvalidArgumentException
|
||||
*/
|
||||
public function __construct(Region $region, $directory, $lockLifetime)
|
||||
{
|
||||
if (! is_dir($directory) && ! @mkdir($directory, 0775, true)) {
|
||||
throw new InvalidArgumentException(sprintf('The directory "%s" does not exist and could not be created.', $directory));
|
||||
}
|
||||
|
||||
if (! is_writable($directory)) {
|
||||
throw new InvalidArgumentException(sprintf('The directory "%s" is not writable.', $directory));
|
||||
}
|
||||
|
||||
$this->region = $region;
|
||||
$this->directory = $directory;
|
||||
$this->lockLifetime = $lockLifetime;
|
||||
}
|
||||
|
||||
private function isLocked(CacheKey $key, ?Lock $lock = null): bool
|
||||
{
|
||||
$filename = $this->getLockFileName($key);
|
||||
|
||||
if (! is_file($filename)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$time = $this->getLockTime($filename);
|
||||
$content = $this->getLockContent($filename);
|
||||
|
||||
if (! $content || ! $time) {
|
||||
@unlink($filename);
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
if ($lock && $content === $lock->value) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// outdated lock
|
||||
if ($time + $this->lockLifetime <= time()) {
|
||||
@unlink($filename);
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private function getLockFileName(CacheKey $key): string
|
||||
{
|
||||
return $this->directory . DIRECTORY_SEPARATOR . $key->hash . '.' . self::LOCK_EXTENSION;
|
||||
}
|
||||
|
||||
/** @return string|false */
|
||||
private function getLockContent(string $filename)
|
||||
{
|
||||
return @file_get_contents($filename);
|
||||
}
|
||||
|
||||
/** @return int|false */
|
||||
private function getLockTime(string $filename)
|
||||
{
|
||||
return @fileatime($filename);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
public function getName()
|
||||
{
|
||||
return $this->region->getName();
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
public function contains(CacheKey $key)
|
||||
{
|
||||
if ($this->isLocked($key)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return $this->region->contains($key);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
public function get(CacheKey $key)
|
||||
{
|
||||
if ($this->isLocked($key)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return $this->region->get($key);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
public function getMultiple(CollectionCacheEntry $collection)
|
||||
{
|
||||
if (array_filter(array_map([$this, 'isLocked'], $collection->identifiers))) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return $this->region->getMultiple($collection);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
public function put(CacheKey $key, CacheEntry $entry, ?Lock $lock = null)
|
||||
{
|
||||
if ($this->isLocked($key, $lock)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return $this->region->put($key, $entry);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
public function evict(CacheKey $key)
|
||||
{
|
||||
if ($this->isLocked($key)) {
|
||||
@unlink($this->getLockFileName($key));
|
||||
}
|
||||
|
||||
return $this->region->evict($key);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
public function evictAll()
|
||||
{
|
||||
// The check below is necessary because on some platforms glob returns false
|
||||
// when nothing matched (even though no errors occurred)
|
||||
$filenames = glob(sprintf('%s/*.%s', $this->directory, self::LOCK_EXTENSION));
|
||||
|
||||
if ($filenames) {
|
||||
foreach ($filenames as $filename) {
|
||||
@unlink($filename);
|
||||
}
|
||||
}
|
||||
|
||||
return $this->region->evictAll();
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
public function lock(CacheKey $key)
|
||||
{
|
||||
if ($this->isLocked($key)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$lock = Lock::createLockRead();
|
||||
$filename = $this->getLockFileName($key);
|
||||
|
||||
if (! @file_put_contents($filename, $lock->value, LOCK_EX)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
chmod($filename, 0664);
|
||||
|
||||
return $lock;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
public function unlock(CacheKey $key, Lock $lock)
|
||||
{
|
||||
if ($this->isLocked($key, $lock)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return @unlink($this->getLockFileName($key));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Doctrine\ORM\Cache\Region;
|
||||
|
||||
use Doctrine\ORM\Cache\CacheKey;
|
||||
use Doctrine\ORM\Cache\TimestampCacheEntry;
|
||||
use Doctrine\ORM\Cache\TimestampRegion;
|
||||
|
||||
/**
|
||||
* Tracks the timestamps of the most recent updates to particular keys.
|
||||
*/
|
||||
class UpdateTimestampCache extends DefaultRegion implements TimestampRegion
|
||||
{
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
public function update(CacheKey $key)
|
||||
{
|
||||
$this->put($key, new TimestampCacheEntry());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,107 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Doctrine\ORM\Cache;
|
||||
|
||||
/**
|
||||
* Cache regions configuration
|
||||
*/
|
||||
class RegionsConfiguration
|
||||
{
|
||||
/** @var array<string,int> */
|
||||
private $lifetimes = [];
|
||||
|
||||
/** @var array<string,int> */
|
||||
private $lockLifetimes = [];
|
||||
|
||||
/** @var int */
|
||||
private $defaultLifetime;
|
||||
|
||||
/** @var int */
|
||||
private $defaultLockLifetime;
|
||||
|
||||
/**
|
||||
* @param int $defaultLifetime
|
||||
* @param int $defaultLockLifetime
|
||||
*/
|
||||
public function __construct($defaultLifetime = 3600, $defaultLockLifetime = 60)
|
||||
{
|
||||
$this->defaultLifetime = (int) $defaultLifetime;
|
||||
$this->defaultLockLifetime = (int) $defaultLockLifetime;
|
||||
}
|
||||
|
||||
/** @return int */
|
||||
public function getDefaultLifetime()
|
||||
{
|
||||
return $this->defaultLifetime;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param int $defaultLifetime
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function setDefaultLifetime($defaultLifetime)
|
||||
{
|
||||
$this->defaultLifetime = (int) $defaultLifetime;
|
||||
}
|
||||
|
||||
/** @return int */
|
||||
public function getDefaultLockLifetime()
|
||||
{
|
||||
return $this->defaultLockLifetime;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param int $defaultLockLifetime
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function setDefaultLockLifetime($defaultLockLifetime)
|
||||
{
|
||||
$this->defaultLockLifetime = (int) $defaultLockLifetime;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $regionName
|
||||
*
|
||||
* @return int
|
||||
*/
|
||||
public function getLifetime($regionName)
|
||||
{
|
||||
return $this->lifetimes[$regionName] ?? $this->defaultLifetime;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $name
|
||||
* @param int $lifetime
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function setLifetime($name, $lifetime)
|
||||
{
|
||||
$this->lifetimes[$name] = (int) $lifetime;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $regionName
|
||||
*
|
||||
* @return int
|
||||
*/
|
||||
public function getLockLifetime($regionName)
|
||||
{
|
||||
return $this->lockLifetimes[$regionName] ?? $this->defaultLockLifetime;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $name
|
||||
* @param int $lifetime
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function setLockLifetime($name, $lifetime)
|
||||
{
|
||||
$this->lockLifetimes[$name] = (int) $lifetime;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Doctrine\ORM\Cache;
|
||||
|
||||
use function microtime;
|
||||
|
||||
/**
|
||||
* Timestamp cache entry
|
||||
*/
|
||||
class TimestampCacheEntry implements CacheEntry
|
||||
{
|
||||
/**
|
||||
* @readonly Public only for performance reasons, it should be considered immutable.
|
||||
* @var float
|
||||
*/
|
||||
public $time;
|
||||
|
||||
/** @param float|null $time */
|
||||
public function __construct($time = null)
|
||||
{
|
||||
$this->time = $time ? (float) $time : microtime(true);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new TimestampCacheEntry
|
||||
*
|
||||
* This method allow Doctrine\Common\Cache\PhpFileCache compatibility
|
||||
*
|
||||
* @param array<string,float> $values array containing property values
|
||||
*
|
||||
* @return TimestampCacheEntry
|
||||
*/
|
||||
public static function __set_state(array $values)
|
||||
{
|
||||
return new self($values['time']);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Doctrine\ORM\Cache;
|
||||
|
||||
/**
|
||||
* A key that identifies a timestamped space.
|
||||
*/
|
||||
class TimestampCacheKey extends CacheKey
|
||||
{
|
||||
/** @param string $space Result cache id */
|
||||
public function __construct($space)
|
||||
{
|
||||
parent::__construct((string) $space);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Doctrine\ORM\Cache;
|
||||
|
||||
use function microtime;
|
||||
|
||||
class TimestampQueryCacheValidator implements QueryCacheValidator
|
||||
{
|
||||
/** @var TimestampRegion */
|
||||
private $timestampRegion;
|
||||
|
||||
public function __construct(TimestampRegion $timestampRegion)
|
||||
{
|
||||
$this->timestampRegion = $timestampRegion;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
public function isValid(QueryCacheKey $key, QueryCacheEntry $entry)
|
||||
{
|
||||
if ($this->regionUpdated($key, $entry)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if ($key->lifetime === 0) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return $entry->time + $key->lifetime > microtime(true);
|
||||
}
|
||||
|
||||
private function regionUpdated(QueryCacheKey $key, QueryCacheEntry $entry): bool
|
||||
{
|
||||
if ($key->timestampKey === null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$timestamp = $this->timestampRegion->get($key->timestampKey);
|
||||
|
||||
return $timestamp && $timestamp->time > $entry->time;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Doctrine\ORM\Cache;
|
||||
|
||||
/**
|
||||
* Defines the contract for a cache region which will specifically be used to store entity "update timestamps".
|
||||
*/
|
||||
interface TimestampRegion extends Region
|
||||
{
|
||||
/**
|
||||
* Update a specific key into the cache region.
|
||||
*
|
||||
* @return void
|
||||
*
|
||||
* @throws LockException Indicates a problem accessing the region.
|
||||
*/
|
||||
public function update(CacheKey $key);
|
||||
}
|
||||
Reference in New Issue
Block a user