Subida del módulo y tema de PrestaShop

This commit is contained in:
Kaloyan
2026-04-09 18:31:51 +02:00
parent 12c253296f
commit 16b3ff9424
39262 changed files with 7418797 additions and 0 deletions

1436
vendor/doctrine/orm/src/AbstractQuery.php vendored Normal file

File diff suppressed because it is too large Load Diff

164
vendor/doctrine/orm/src/Cache.php vendored Normal file
View File

@@ -0,0 +1,164 @@
<?php
declare(strict_types=1);
namespace Doctrine\ORM;
use Doctrine\ORM\Cache\QueryCache;
use Doctrine\ORM\Cache\Region;
/**
* Provides an API for querying/managing the second level cache regions.
*/
interface Cache
{
public const DEFAULT_QUERY_REGION_NAME = 'query_cache_region';
public const DEFAULT_TIMESTAMP_REGION_NAME = 'timestamp_cache_region';
/**
* May read items from the cache, but will not add items.
*/
public const MODE_GET = 1;
/**
* Will never read items from the cache,
* but will add items to the cache as it reads them from the database.
*/
public const MODE_PUT = 2;
/**
* May read items from the cache, and add items to the cache.
*/
public const MODE_NORMAL = 3;
/**
* The query will never read items from the cache,
* but will refresh items to the cache as it reads them from the database.
*/
public const MODE_REFRESH = 4;
/**
* @param string $className The entity class.
*
* @return Region|null
*/
public function getEntityCacheRegion($className);
/**
* @param string $className The entity class.
* @param string $association The field name that represents the association.
*
* @return Region|null
*/
public function getCollectionCacheRegion($className, $association);
/**
* Determine whether the cache contains data for the given entity "instance".
*
* @param string $className The entity class.
* @param mixed $identifier The entity identifier
*
* @return bool true if the underlying cache contains corresponding data; false otherwise.
*/
public function containsEntity($className, $identifier);
/**
* Evicts the entity data for a particular entity "instance".
*
* @param string $className The entity class.
* @param mixed $identifier The entity identifier.
*
* @return void
*/
public function evictEntity($className, $identifier);
/**
* Evicts all entity data from the given region.
*
* @param string $className The entity metadata.
*
* @return void
*/
public function evictEntityRegion($className);
/**
* Evict data from all entity regions.
*
* @return void
*/
public function evictEntityRegions();
/**
* Determine whether the cache contains data for the given collection.
*
* @param string $className The entity class.
* @param string $association The field name that represents the association.
* @param mixed $ownerIdentifier The identifier of the owning entity.
*
* @return bool true if the underlying cache contains corresponding data; false otherwise.
*/
public function containsCollection($className, $association, $ownerIdentifier);
/**
* Evicts the cache data for the given identified collection instance.
*
* @param string $className The entity class.
* @param string $association The field name that represents the association.
* @param mixed $ownerIdentifier The identifier of the owning entity.
*
* @return void
*/
public function evictCollection($className, $association, $ownerIdentifier);
/**
* Evicts all entity data from the given region.
*
* @param string $className The entity class.
* @param string $association The field name that represents the association.
*
* @return void
*/
public function evictCollectionRegion($className, $association);
/**
* Evict data from all collection regions.
*
* @return void
*/
public function evictCollectionRegions();
/**
* Determine whether the cache contains data for the given query.
*
* @param string $regionName The cache name given to the query.
*
* @return bool true if the underlying cache contains corresponding data; false otherwise.
*/
public function containsQuery($regionName);
/**
* Evicts all cached query results under the given name, or default query cache if the region name is NULL.
*
* @param string|null $regionName The cache name associated to the queries being cached.
*
* @return void
*/
public function evictQueryRegion($regionName = null);
/**
* Evict data from all query regions.
*
* @return void
*/
public function evictQueryRegions();
/**
* Get query cache by region name or create a new one if none exist.
*
* @param string|null $regionName Query cache region name, or default query cache if the region name is NULL.
*
* @return QueryCache The Query Cache associated with the region name.
*/
public function getQueryCache($regionName = null);
}

View File

@@ -0,0 +1,51 @@
<?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 class-string
*/
public $class;
/**
* @param class-string $class The entity class.
* @param array<string, mixed> $identifier The entity identifier.
*/
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']);
}
}

View File

@@ -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;
}
}

View File

@@ -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
{
}

View File

@@ -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));
}
}

View File

@@ -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);
}

View File

@@ -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;
}
}
}

View File

@@ -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']);
}
}

View File

@@ -0,0 +1,56 @@
<?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 class-string
*/
public $entityClass;
/**
* The association name
*
* @readonly Public only for performance reasons, it should be considered immutable.
* @var string
*/
public $association;
/**
* @param class-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.
*/
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);
}
}

View File

@@ -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);
}

View File

@@ -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);
}

View File

@@ -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];
}
}

View File

@@ -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);
}
}

View File

@@ -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;
}
}

View File

@@ -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;
}
}

View File

@@ -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;
}
}

View File

@@ -0,0 +1,71 @@
<?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 class-string
*/
public $class;
/**
* @param class-string $class The entity class.
* @param array<string,mixed> $data The entity data.
*/
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);
}
}

View File

@@ -0,0 +1,46 @@
<?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 class-string
*/
public $entityClass;
/**
* @param class-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
*/
public function __construct($entityClass, array $identifier)
{
ksort($identifier);
$this->identifier = $identifier;
$this->entityClass = $entityClass;
parent::__construct(str_replace('\\', '.', strtolower($entityClass) . '_' . implode(' ', $identifier)));
}
}

View File

@@ -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);
}

View File

@@ -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
{
}

View File

@@ -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
));
}
}

View File

@@ -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));
}
}

View File

@@ -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.');
}
}

View File

@@ -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.'
);
}
}

View File

@@ -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.');
}
}

View File

@@ -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) . '.'
);
}
}

View File

@@ -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
));
}
}

View File

@@ -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
));
}
}

View File

@@ -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.');
}
}

View File

@@ -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
vendor/doctrine/orm/src/Cache/Lock.php vendored Normal file
View File

@@ -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));
}
}

View File

@@ -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
{
}

View File

@@ -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);
}

View File

@@ -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);
}
}
}

View File

@@ -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);
}
}

View File

@@ -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);
}

View File

@@ -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();
}

View File

@@ -0,0 +1,281 @@
<?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 class-string $targetEntity
* @param object $element
*
* @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);
}
}
}

View File

@@ -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);
}

View File

@@ -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,
];
}
}

View File

@@ -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);
}
}

View File

@@ -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,
];
}
}

View File

@@ -0,0 +1,622 @@
<?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\Internal\CriteriaOrderings;
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
{
use CriteriaOrderings;
/** @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 = self::getCriteriaOrderings($criteria);
$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);
}
}

View File

@@ -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);
}

View File

@@ -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;
}
}

View File

@@ -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));
}
}

View File

@@ -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,
];
}
}

View File

@@ -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();
}

View File

@@ -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']);
}
}

View File

@@ -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);
}
}

View File

@@ -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);
}

View File

@@ -0,0 +1,75 @@
<?php
declare(strict_types=1);
namespace Doctrine\ORM\Cache;
use Doctrine\ORM\Cache\Exception\CacheException;
/**
* Defines a contract for accessing a particular named region.
*
* @phpstan-ignore interface.extendsDeprecatedInterface
*/
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();
}

View File

@@ -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
{
}

View File

@@ -0,0 +1,215 @@
<?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
);
// @phpstan-ignore property.deprecated
$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 {
// @phpstan-ignore property.deprecated
$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, '{}()/\@:', '________');
}
}

View File

@@ -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));
}
}

View File

@@ -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());
}
}

View File

@@ -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;
}
}

View File

@@ -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']);
}
}

View File

@@ -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);
}
}

View File

@@ -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;
}
}

View File

@@ -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);
}

1148
vendor/doctrine/orm/src/Configuration.php vendored Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,326 @@
<?php
declare(strict_types=1);
namespace Doctrine\ORM\Decorator;
use Doctrine\Deprecations\Deprecation;
use Doctrine\ORM\EntityManagerInterface;
use Doctrine\ORM\EntityRepository;
use Doctrine\ORM\Query\ResultSetMapping;
use Doctrine\Persistence\ObjectManagerDecorator;
use function func_get_arg;
use function func_num_args;
use function get_debug_type;
use function method_exists;
use function sprintf;
use function trigger_error;
use const E_USER_NOTICE;
/**
* Base class for EntityManager decorators
*
* @extends ObjectManagerDecorator<EntityManagerInterface>
*/
abstract class EntityManagerDecorator extends ObjectManagerDecorator implements EntityManagerInterface
{
public function __construct(EntityManagerInterface $wrapped)
{
$this->wrapped = $wrapped;
}
/**
* {@inheritDoc}
*/
public function getConnection()
{
return $this->wrapped->getConnection();
}
/**
* {@inheritDoc}
*/
public function getExpressionBuilder()
{
return $this->wrapped->getExpressionBuilder();
}
/**
* {@inheritDoc}
*
* @param class-string<T> $className
*
* @psalm-return EntityRepository<T>
*
* @template T of object
*/
public function getRepository($className)
{
return $this->wrapped->getRepository($className);
}
/**
* {@inheritDoc}
*/
public function getClassMetadata($className)
{
return $this->wrapped->getClassMetadata($className);
}
/**
* {@inheritDoc}
*/
public function beginTransaction()
{
$this->wrapped->beginTransaction();
}
/**
* {@inheritDoc}
*/
public function transactional($func)
{
return $this->wrapped->transactional($func);
}
/**
* {@inheritDoc}
*/
public function wrapInTransaction(callable $func)
{
if (! method_exists($this->wrapped, 'wrapInTransaction')) {
trigger_error(
sprintf('Calling `transactional()` instead of `wrapInTransaction()` which is not implemented on %s', get_debug_type($this->wrapped)),
E_USER_NOTICE
);
// @phpstan-ignore method.deprecated
return $this->wrapped->transactional($func);
}
return $this->wrapped->wrapInTransaction($func);
}
/**
* {@inheritDoc}
*/
public function commit()
{
$this->wrapped->commit();
}
/**
* {@inheritDoc}
*/
public function rollback()
{
$this->wrapped->rollback();
}
/**
* {@inheritDoc}
*/
public function createQuery($dql = '')
{
return $this->wrapped->createQuery($dql);
}
/**
* {@inheritDoc}
*/
public function createNamedQuery($name)
{
return $this->wrapped->createNamedQuery($name);
}
/**
* {@inheritDoc}
*/
public function createNativeQuery($sql, ResultSetMapping $rsm)
{
return $this->wrapped->createNativeQuery($sql, $rsm);
}
/**
* {@inheritDoc}
*/
public function createNamedNativeQuery($name)
{
return $this->wrapped->createNamedNativeQuery($name);
}
/**
* {@inheritDoc}
*/
public function createQueryBuilder()
{
return $this->wrapped->createQueryBuilder();
}
/**
* {@inheritDoc}
*/
public function getReference($entityName, $id)
{
return $this->wrapped->getReference($entityName, $id);
}
/**
* {@inheritDoc}
*/
public function getPartialReference($entityName, $identifier)
{
Deprecation::trigger(
'doctrine/orm',
'https://github.com/doctrine/orm/pull/10987',
'Method %s is deprecated and will be removed in 3.0.',
__METHOD__
);
return $this->wrapped->getPartialReference($entityName, $identifier);
}
/**
* {@inheritDoc}
*/
public function close()
{
$this->wrapped->close();
}
/**
* {@inheritDoc}
*/
public function copy($entity, $deep = false)
{
return $this->wrapped->copy($entity, $deep);
}
/**
* {@inheritDoc}
*/
public function lock($entity, $lockMode, $lockVersion = null)
{
$this->wrapped->lock($entity, $lockMode, $lockVersion);
}
/**
* {@inheritDoc}
*/
public function find($className, $id, $lockMode = null, $lockVersion = null)
{
return $this->wrapped->find($className, $id, $lockMode, $lockVersion);
}
/**
* {@inheritDoc}
*/
public function flush($entity = null)
{
$this->wrapped->flush($entity);
}
/**
* {@inheritDoc}
*/
public function refresh($object)
{
$lockMode = null;
if (func_num_args() > 1) {
$lockMode = func_get_arg(1);
}
$this->wrapped->refresh($object, $lockMode);
}
/**
* {@inheritDoc}
*/
public function getEventManager()
{
return $this->wrapped->getEventManager();
}
/**
* {@inheritDoc}
*/
public function getConfiguration()
{
return $this->wrapped->getConfiguration();
}
/**
* {@inheritDoc}
*/
public function isOpen()
{
return $this->wrapped->isOpen();
}
/**
* {@inheritDoc}
*/
public function getUnitOfWork()
{
return $this->wrapped->getUnitOfWork();
}
/**
* {@inheritDoc}
*/
public function getHydrator($hydrationMode)
{
return $this->wrapped->getHydrator($hydrationMode);
}
/**
* {@inheritDoc}
*/
public function newHydrator($hydrationMode)
{
return $this->wrapped->newHydrator($hydrationMode);
}
/**
* {@inheritDoc}
*/
public function getProxyFactory()
{
return $this->wrapped->getProxyFactory();
}
/**
* {@inheritDoc}
*/
public function getFilters()
{
return $this->wrapped->getFilters();
}
/**
* {@inheritDoc}
*/
public function isFiltersStateClean()
{
return $this->wrapped->isFiltersStateClean();
}
/**
* {@inheritDoc}
*/
public function hasFilters()
{
return $this->wrapped->hasFilters();
}
/**
* {@inheritDoc}
*/
public function getCache()
{
return $this->wrapped->getCache();
}
}

1129
vendor/doctrine/orm/src/EntityManager.php vendored Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,344 @@
<?php
declare(strict_types=1);
namespace Doctrine\ORM;
use BadMethodCallException;
use DateTimeInterface;
use Doctrine\Common\EventManager;
use Doctrine\DBAL\Connection;
use Doctrine\DBAL\LockMode;
use Doctrine\ORM\Exception\ORMException;
use Doctrine\ORM\Internal\Hydration\AbstractHydrator;
use Doctrine\ORM\Proxy\ProxyFactory;
use Doctrine\ORM\Query\Expr;
use Doctrine\ORM\Query\FilterCollection;
use Doctrine\ORM\Query\ResultSetMapping;
use Doctrine\Persistence\ObjectManager;
/**
* EntityManager interface
*
* @method Mapping\ClassMetadataFactory getMetadataFactory()
* @method mixed wrapInTransaction(callable $func)
* @method void refresh(object $object, ?int $lockMode = null)
*/
interface EntityManagerInterface extends ObjectManager
{
/**
* {@inheritDoc}
*
* @param class-string<T> $className
*
* @return EntityRepository<T>
*
* @template T of object
*/
public function getRepository($className);
/**
* Returns the cache API for managing the second level cache regions or NULL if the cache is not enabled.
*
* @return Cache|null
*/
public function getCache();
/**
* Gets the database connection object used by the EntityManager.
*
* @return Connection
*/
public function getConnection();
/**
* Gets an ExpressionBuilder used for object-oriented construction of query expressions.
*
* Example:
*
* <code>
* $qb = $em->createQueryBuilder();
* $expr = $em->getExpressionBuilder();
* $qb->select('u')->from('User', 'u')
* ->where($expr->orX($expr->eq('u.id', 1), $expr->eq('u.id', 2)));
* </code>
*
* @return Expr
*/
public function getExpressionBuilder();
/**
* Starts a transaction on the underlying database connection.
*
* @return void
*/
public function beginTransaction();
/**
* Executes a function in a transaction.
*
* The function gets passed this EntityManager instance as an (optional) parameter.
*
* {@link flush} is invoked prior to transaction commit.
*
* If an exception occurs during execution of the function or flushing or transaction commit,
* the transaction is rolled back, the EntityManager closed and the exception re-thrown.
*
* @deprecated 2.10 Use {@link wrapInTransaction} instead.
*
* @param callable $func The function to execute transactionally.
*
* @return mixed The non-empty value returned from the closure or true instead.
*/
public function transactional($func);
/**
* Executes a function in a transaction.
*
* The function gets passed this EntityManager instance as an (optional) parameter.
*
* {@link flush} is invoked prior to transaction commit.
*
* If an exception occurs during execution of the function or flushing or transaction commit,
* the transaction is rolled back, the EntityManager closed and the exception re-thrown.
*
* @param callable(self): T $func The function to execute transactionally.
*
* @return T The value returned from the closure.
*
* @template T
*/
// public function wrapInTransaction(callable $func);
/**
* Commits a transaction on the underlying database connection.
*
* @return void
*/
public function commit();
/**
* Performs a rollback on the underlying database connection.
*
* @return void
*/
public function rollback();
/**
* Creates a new Query object.
*
* @param string $dql The DQL string.
*
* @return Query
*/
public function createQuery($dql = '');
/**
* Creates a Query from a named query.
*
* @param string $name
*
* @return Query
*/
public function createNamedQuery($name);
/**
* Creates a native SQL query.
*
* @param string $sql
* @param ResultSetMapping $rsm The ResultSetMapping to use.
*
* @return NativeQuery
*/
public function createNativeQuery($sql, ResultSetMapping $rsm);
/**
* Creates a NativeQuery from a named native query.
*
* @param string $name
*
* @return NativeQuery
*/
public function createNamedNativeQuery($name);
/**
* Create a QueryBuilder instance
*
* @return QueryBuilder
*/
public function createQueryBuilder();
/**
* Gets a reference to the entity identified by the given type and identifier
* without actually loading it, if the entity is not yet loaded.
*
* @param class-string<T> $entityName The name of the entity type.
* @param mixed $id The entity identifier.
*
* @return T|null The entity reference.
*
* @throws ORMException
*
* @template T
*/
public function getReference($entityName, $id);
/**
* Gets a partial reference to the entity identified by the given type and identifier
* without actually loading it, if the entity is not yet loaded.
*
* The returned reference may be a partial object if the entity is not yet loaded/managed.
* If it is a partial object it will not initialize the rest of the entity state on access.
* Thus you can only ever safely access the identifier of an entity obtained through
* this method.
*
* The use-cases for partial references involve maintaining bidirectional associations
* without loading one side of the association or to update an entity without loading it.
* Note, however, that in the latter case the original (persistent) entity data will
* never be visible to the application (especially not event listeners) as it will
* never be loaded in the first place.
*
* @deprecated 2.7 This method is being removed from the ORM and won't have any replacement
*
* @param class-string<T> $entityName The name of the entity type.
* @param mixed $identifier The entity identifier.
*
* @return T|null The (partial) entity reference
*
* @template T
*/
public function getPartialReference($entityName, $identifier);
/**
* Closes the EntityManager. All entities that are currently managed
* by this EntityManager become detached. The EntityManager may no longer
* be used after it is closed.
*
* @return void
*/
public function close();
/**
* Creates a copy of the given entity. Can create a shallow or a deep copy.
*
* @deprecated 2.7 This method is being removed from the ORM and won't have any replacement
*
* @param object $entity The entity to copy.
* @param bool $deep FALSE for a shallow copy, TRUE for a deep copy.
*
* @return object The new entity.
*
* @throws BadMethodCallException
*/
public function copy($entity, $deep = false);
/**
* Acquire a lock on the given entity.
*
* @param object $entity
* @param int $lockMode
* @param int|DateTimeInterface|null $lockVersion
* @psalm-param LockMode::* $lockMode
*
* @return void
*
* @throws OptimisticLockException
* @throws PessimisticLockException
*/
public function lock($entity, $lockMode, $lockVersion = null);
/**
* Gets the EventManager used by the EntityManager.
*
* @return EventManager
*/
public function getEventManager();
/**
* Gets the Configuration used by the EntityManager.
*
* @return Configuration
*/
public function getConfiguration();
/**
* Check if the Entity manager is open or closed.
*
* @return bool
*/
public function isOpen();
/**
* Gets the UnitOfWork used by the EntityManager to coordinate operations.
*
* @return UnitOfWork
*/
public function getUnitOfWork();
/**
* Gets a hydrator for the given hydration mode.
*
* This method caches the hydrator instances which is used for all queries that don't
* selectively iterate over the result.
*
* @deprecated
*
* @param string|int $hydrationMode
* @psalm-param string|AbstractQuery::HYDRATE_* $hydrationMode
*
* @return AbstractHydrator
*/
public function getHydrator($hydrationMode);
/**
* Create a new instance for the given hydration mode.
*
* @param string|int $hydrationMode
* @psalm-param string|AbstractQuery::HYDRATE_* $hydrationMode
*
* @return AbstractHydrator
*
* @throws ORMException
*/
public function newHydrator($hydrationMode);
/**
* Gets the proxy factory used by the EntityManager to create entity proxies.
*
* @return ProxyFactory
*/
public function getProxyFactory();
/**
* Gets the enabled filters.
*
* @return FilterCollection The active filter collection.
*/
public function getFilters();
/**
* Checks whether the state of the filter collection is clean.
*
* @return bool True, if the filter collection is clean.
*/
public function isFiltersStateClean();
/**
* Checks whether the Entity Manager has filters.
*
* @return bool True, if the EM has a filter collection.
*/
public function hasFilters();
/**
* {@inheritDoc}
*
* @param string|class-string<T> $className
*
* @return Mapping\ClassMetadata
* @psalm-return ($className is class-string<T> ? Mapping\ClassMetadata<T> : Mapping\ClassMetadata<object>)
*
* @psalm-template T of object
*/
public function getClassMetadata($className);
}

View File

@@ -0,0 +1,48 @@
<?php
declare(strict_types=1);
namespace Doctrine\ORM;
use Doctrine\ORM\Exception\ORMException;
use function implode;
use function sprintf;
/**
* Exception thrown when a Proxy fails to retrieve an Entity result.
*/
class EntityNotFoundException extends ORMException
{
/**
* Static constructor.
*
* @param string $className
* @param string[] $id
*
* @return self
*/
public static function fromClassNameAndIdentifier($className, array $id)
{
$ids = [];
foreach ($id as $key => $value) {
$ids[] = $key . '(' . $value . ')';
}
return new self(
'Entity of type \'' . $className . '\'' . ($ids ? ' for IDs ' . implode(', ', $ids) : '') . ' was not found'
);
}
/**
* Instance for which no identifier can be found
*/
public static function noIdentifierFound(string $className): self
{
return new self(sprintf(
'Unable to find "%s" entity identifier associated with the UnitOfWork',
$className
));
}
}

View File

@@ -0,0 +1,366 @@
<?php
declare(strict_types=1);
namespace Doctrine\ORM;
use BadMethodCallException;
use Doctrine\Common\Collections\AbstractLazyCollection;
use Doctrine\Common\Collections\Criteria;
use Doctrine\Common\Collections\Selectable;
use Doctrine\Common\Persistence\PersistentObject;
use Doctrine\DBAL\LockMode;
use Doctrine\Deprecations\Deprecation;
use Doctrine\Inflector\Inflector;
use Doctrine\Inflector\InflectorFactory;
use Doctrine\ORM\Exception\NotSupported;
use Doctrine\ORM\Mapping\ClassMetadata;
use Doctrine\ORM\Query\ResultSetMappingBuilder;
use Doctrine\ORM\Repository\Exception\InvalidMagicMethodCall;
use Doctrine\Persistence\ObjectRepository;
use function array_slice;
use function class_exists;
use function lcfirst;
use function sprintf;
use function str_starts_with;
use function substr;
/**
* An EntityRepository serves as a repository for entities with generic as well as
* business specific methods for retrieving entities.
*
* This class is designed for inheritance and users can subclass this class to
* write their own repositories with business-specific methods to locate entities.
*
* @template T of object
* @template-implements Selectable<int,T>
* @template-implements ObjectRepository<T>
*/
class EntityRepository implements ObjectRepository, Selectable
{
/**
* @internal This property will be private in 3.0, call {@see getEntityName()} instead.
*
* @var class-string<T>
*/
protected $_entityName;
/**
* @internal This property will be private in 3.0, call {@see getEntityManager()} instead.
*
* @var EntityManagerInterface
*/
protected $_em;
/**
* @internal This property will be private in 3.0, call {@see getClassMetadata()} instead.
*
* @var ClassMetadata
* @psalm-var ClassMetadata<T>
*/
protected $_class;
/** @var Inflector|null */
private static $inflector;
/** @psalm-param ClassMetadata<T> $class */
public function __construct(EntityManagerInterface $em, ClassMetadata $class)
{
$this->_entityName = $class->name;
$this->_em = $em;
$this->_class = $class;
}
/**
* Creates a new QueryBuilder instance that is prepopulated for this entity name.
*
* @param string $alias
* @param string|null $indexBy The index for the from.
*
* @return QueryBuilder
*/
public function createQueryBuilder($alias, $indexBy = null)
{
return $this->_em->createQueryBuilder()
->select($alias)
->from($this->_entityName, $alias, $indexBy);
}
/**
* Creates a new result set mapping builder for this entity.
*
* The column naming strategy is "INCREMENT".
*
* @param string $alias
*
* @return ResultSetMappingBuilder
*/
public function createResultSetMappingBuilder($alias)
{
$rsm = new ResultSetMappingBuilder($this->_em, ResultSetMappingBuilder::COLUMN_RENAMING_INCREMENT);
$rsm->addRootEntityFromClassMetadata($this->_entityName, $alias);
return $rsm;
}
/**
* Creates a new Query instance based on a predefined metadata named query.
*
* @deprecated
*
* @param string $queryName
*
* @return Query
*/
public function createNamedQuery($queryName)
{
Deprecation::trigger(
'doctrine/orm',
'https://github.com/doctrine/orm/issues/8592',
'Named Queries are deprecated, here "%s" on entity %s. Move the query logic into EntityRepository',
$queryName,
$this->_class->name
);
return $this->_em->createQuery($this->_class->getNamedQuery($queryName));
}
/**
* Creates a native SQL query.
*
* @deprecated
*
* @param string $queryName
*
* @return NativeQuery
*/
public function createNativeNamedQuery($queryName)
{
Deprecation::trigger(
'doctrine/orm',
'https://github.com/doctrine/orm/issues/8592',
'Named Native Queries are deprecated, here "%s" on entity %s. Move the query logic into EntityRepository',
$queryName,
$this->_class->name
);
$queryMapping = $this->_class->getNamedNativeQuery($queryName);
$rsm = new Query\ResultSetMappingBuilder($this->_em);
$rsm->addNamedNativeQueryMapping($this->_class, $queryMapping);
return $this->_em->createNativeQuery($queryMapping['query'], $rsm);
}
/**
* Clears the repository, causing all managed entities to become detached.
*
* @deprecated 2.8 This method is being removed from the ORM and won't have any replacement
*
* @return void
*/
public function clear()
{
Deprecation::trigger(
'doctrine/orm',
'https://github.com/doctrine/orm/issues/8460',
'Calling %s() is deprecated and will not be supported in Doctrine ORM 3.0.',
__METHOD__
);
if (! class_exists(PersistentObject::class)) {
throw NotSupported::createForPersistence3(sprintf(
'Partial clearing of entities for class %s',
$this->_class->rootEntityName
));
}
$this->_em->clear($this->_class->rootEntityName);
}
/**
* Finds an entity by its primary key / identifier.
*
* @param mixed $id The identifier.
* @param int|null $lockMode One of the \Doctrine\DBAL\LockMode::* constants
* or NULL if no specific lock mode should be used
* during the search.
* @param int|null $lockVersion The lock version.
* @psalm-param LockMode::*|null $lockMode
*
* @return object|null The entity instance or NULL if the entity can not be found.
* @psalm-return ?T
*/
public function find($id, $lockMode = null, $lockVersion = null)
{
return $this->_em->find($this->_entityName, $id, $lockMode, $lockVersion);
}
/**
* Finds all entities in the repository.
*
* @psalm-return list<T> The entities.
*/
public function findAll()
{
return $this->findBy([]);
}
/**
* Finds entities by a set of criteria.
*
* @param int|null $limit
* @param int|null $offset
* @psalm-param array<string, mixed> $criteria
* @psalm-param array<string, string>|null $orderBy
*
* @return object[] The objects.
* @psalm-return list<T>
*/
public function findBy(array $criteria, ?array $orderBy = null, $limit = null, $offset = null)
{
$persister = $this->_em->getUnitOfWork()->getEntityPersister($this->_entityName);
return $persister->loadAll($criteria, $orderBy, $limit, $offset);
}
/**
* Finds a single entity by a set of criteria.
*
* @psalm-param array<string, mixed> $criteria
* @psalm-param array<string, string>|null $orderBy
*
* @return object|null The entity instance or NULL if the entity can not be found.
* @psalm-return ?T
*/
public function findOneBy(array $criteria, ?array $orderBy = null)
{
$persister = $this->_em->getUnitOfWork()->getEntityPersister($this->_entityName);
return $persister->load($criteria, null, null, [], null, 1, $orderBy);
}
/**
* Counts entities by a set of criteria.
*
* @psalm-param array<string, mixed> $criteria
*
* @return int The cardinality of the objects that match the given criteria.
*
* @todo Add this method to `ObjectRepository` interface in the next major release
*/
public function count(array $criteria)
{
return $this->_em->getUnitOfWork()->getEntityPersister($this->_entityName)->count($criteria);
}
/**
* Adds support for magic method calls.
*
* @param string $method
* @param mixed[] $arguments
* @psalm-param list<mixed> $arguments
*
* @return mixed The returned value from the resolved method.
*
* @throws BadMethodCallException If the method called is invalid.
*/
public function __call($method, $arguments)
{
if (str_starts_with($method, 'findBy')) {
return $this->resolveMagicCall('findBy', substr($method, 6), $arguments);
}
if (str_starts_with($method, 'findOneBy')) {
return $this->resolveMagicCall('findOneBy', substr($method, 9), $arguments);
}
if (str_starts_with($method, 'countBy')) {
return $this->resolveMagicCall('count', substr($method, 7), $arguments);
}
throw new BadMethodCallException(sprintf(
'Undefined method "%s". The method name must start with ' .
'either findBy, findOneBy or countBy!',
$method
));
}
/** @return class-string<T> */
protected function getEntityName()
{
return $this->_entityName;
}
/**
* {@inheritDoc}
*/
public function getClassName()
{
return $this->getEntityName();
}
/** @return EntityManagerInterface */
protected function getEntityManager()
{
return $this->_em;
}
/**
* @return ClassMetadata
* @psalm-return ClassMetadata<T>
*/
protected function getClassMetadata()
{
return $this->_class;
}
/**
* Select all elements from a selectable that match the expression and
* return a new collection containing these elements.
*
* @return AbstractLazyCollection
* @psalm-return AbstractLazyCollection<int, T>&Selectable<int, T>
*/
public function matching(Criteria $criteria)
{
$persister = $this->_em->getUnitOfWork()->getEntityPersister($this->_entityName);
return new LazyCriteriaCollection($persister, $criteria);
}
/**
* Resolves a magic method call to the proper existent method at `EntityRepository`.
*
* @param string $method The method to call
* @param string $by The property name used as condition
* @psalm-param list<mixed> $arguments The arguments to pass at method call
*
* @return mixed
*
* @throws InvalidMagicMethodCall If the method called is invalid or the
* requested field/association does not exist.
*/
private function resolveMagicCall(string $method, string $by, array $arguments)
{
if (! $arguments) {
throw InvalidMagicMethodCall::onMissingParameter($method . $by);
}
if (self::$inflector === null) {
self::$inflector = InflectorFactory::create()->build();
}
$fieldName = lcfirst(self::$inflector->classify($by));
if (! ($this->_class->hasField($fieldName) || $this->_class->hasAssociation($fieldName))) {
throw InvalidMagicMethodCall::becauseFieldNotFoundIn(
$this->_entityName,
$fieldName,
$method . $by
);
}
return $this->$method([$fieldName => $arguments[0]], ...array_slice($arguments, 1));
}
}

View File

@@ -0,0 +1,72 @@
<?php
declare(strict_types=1);
namespace Doctrine\ORM\Event;
use Doctrine\Deprecations\Deprecation;
use Doctrine\ORM\EntityManagerInterface;
use Doctrine\Persistence\Event\LifecycleEventArgs as BaseLifecycleEventArgs;
/**
* Lifecycle Events are triggered by the UnitOfWork during lifecycle transitions
* of entities.
*
* @deprecated This class will be removed in ORM 3.0. Use one of the dedicated classes instead.
*
* @extends BaseLifecycleEventArgs<EntityManagerInterface>
*/
class LifecycleEventArgs extends BaseLifecycleEventArgs
{
/** @param object $object */
public function __construct($object, EntityManagerInterface $objectManager)
{
Deprecation::triggerIfCalledFromOutside(
'doctrine/orm',
'https://github.com/doctrine/orm/issues/9875',
'The %s class is deprecated and will be removed in ORM 3.0. Use %s instead.',
self::class,
BaseLifecycleEventArgs::class
);
parent::__construct($object, $objectManager);
}
/**
* Retrieves associated Entity.
*
* @deprecated 2.13. Use {@see getObject} instead.
*
* @return object
*/
public function getEntity()
{
Deprecation::trigger(
'doctrine/orm',
'https://github.com/doctrine/orm/issues/9875',
'Method %s() is deprecated and will be removed in Doctrine ORM 3.0. Use getObject() instead.',
__METHOD__
);
return $this->getObject();
}
/**
* Retrieves associated EntityManager.
*
* @deprecated 2.13. Use {@see getObjectManager} instead.
*
* @return EntityManagerInterface
*/
public function getEntityManager()
{
Deprecation::trigger(
'doctrine/orm',
'https://github.com/doctrine/orm/issues/9875',
'Method %s() is deprecated and will be removed in Doctrine ORM 3.0. Use getObjectManager() instead.',
__METHOD__
);
return $this->getObjectManager();
}
}

View File

@@ -0,0 +1,101 @@
<?php
declare(strict_types=1);
namespace Doctrine\ORM\Event;
use Doctrine\Common\EventArgs;
use Doctrine\Common\EventManager;
use Doctrine\ORM\EntityManagerInterface;
use Doctrine\ORM\Mapping\ClassMetadata;
use Doctrine\ORM\Mapping\EntityListenerResolver;
/**
* A method invoker based on entity lifecycle.
*/
class ListenersInvoker
{
public const INVOKE_NONE = 0;
public const INVOKE_LISTENERS = 1;
public const INVOKE_CALLBACKS = 2;
public const INVOKE_MANAGER = 4;
/** @var EntityListenerResolver The Entity listener resolver. */
private $resolver;
/**
* The EventManager used for dispatching events.
*
* @var EventManager
*/
private $eventManager;
public function __construct(EntityManagerInterface $em)
{
$this->eventManager = $em->getEventManager();
$this->resolver = $em->getConfiguration()->getEntityListenerResolver();
}
/**
* Get the subscribed event systems
*
* @param ClassMetadata $metadata The entity metadata.
* @param string $eventName The entity lifecycle event.
*
* @return int Bitmask of subscribed event systems.
* @psalm-return int-mask-of<self::INVOKE_*>
*/
public function getSubscribedSystems(ClassMetadata $metadata, $eventName)
{
$invoke = self::INVOKE_NONE;
if (isset($metadata->lifecycleCallbacks[$eventName])) {
$invoke |= self::INVOKE_CALLBACKS;
}
if (isset($metadata->entityListeners[$eventName])) {
$invoke |= self::INVOKE_LISTENERS;
}
if ($this->eventManager->hasListeners($eventName)) {
$invoke |= self::INVOKE_MANAGER;
}
return $invoke;
}
/**
* Dispatches the lifecycle event of the given entity.
*
* @param ClassMetadata $metadata The entity metadata.
* @param string $eventName The entity lifecycle event.
* @param object $entity The Entity on which the event occurred.
* @param EventArgs $event The Event args.
* @param int $invoke Bitmask to invoke listeners.
* @psalm-param int-mask-of<self::INVOKE_*> $invoke
*
* @return void
*/
public function invoke(ClassMetadata $metadata, $eventName, $entity, EventArgs $event, $invoke)
{
if ($invoke & self::INVOKE_CALLBACKS) {
foreach ($metadata->lifecycleCallbacks[$eventName] as $callback) {
$entity->$callback($event);
}
}
if ($invoke & self::INVOKE_LISTENERS) {
foreach ($metadata->entityListeners[$eventName] as $listener) {
$class = $listener['class'];
$method = $listener['method'];
$instance = $this->resolver->resolve($class);
$instance->$method($entity, $event);
}
}
if ($invoke & self::INVOKE_MANAGER) {
$this->eventManager->dispatchEvent($eventName, $event);
}
}
}

View File

@@ -0,0 +1,27 @@
<?php
declare(strict_types=1);
namespace Doctrine\ORM\Event;
use Doctrine\ORM\EntityManagerInterface;
use Doctrine\ORM\Mapping\ClassMetadata;
use Doctrine\Persistence\Event\LoadClassMetadataEventArgs as BaseLoadClassMetadataEventArgs;
/**
* Class that holds event arguments for a loadMetadata event.
*
* @extends BaseLoadClassMetadataEventArgs<ClassMetadata<object>, EntityManagerInterface>
*/
class LoadClassMetadataEventArgs extends BaseLoadClassMetadataEventArgs
{
/**
* Retrieve associated EntityManager.
*
* @return EntityManagerInterface
*/
public function getEntityManager()
{
return $this->getObjectManager();
}
}

View File

@@ -0,0 +1,72 @@
<?php
declare(strict_types=1);
namespace Doctrine\ORM\Event;
use Doctrine\Deprecations\Deprecation;
use Doctrine\ORM\EntityManagerInterface;
use Doctrine\Persistence\Event\ManagerEventArgs;
use Doctrine\Persistence\Mapping\ClassMetadata;
use Doctrine\Persistence\ObjectManager;
use function func_num_args;
/**
* Class that holds event arguments for a `onClassMetadataNotFound` event.
*
* This object is mutable by design, allowing callbacks having access to it to set the
* found metadata in it, and therefore "cancelling" a `onClassMetadataNotFound` event
*
* @extends ManagerEventArgs<EntityManagerInterface>
*/
class OnClassMetadataNotFoundEventArgs extends ManagerEventArgs
{
/** @var string */
private $className;
/** @var ClassMetadata|null */
private $foundMetadata;
/**
* @param string $className
* @param EntityManagerInterface $objectManager
*/
public function __construct($className, ObjectManager $objectManager)
{
$this->className = (string) $className;
parent::__construct($objectManager);
}
/** @return void */
public function setFoundMetadata(?ClassMetadata $classMetadata = null)
{
if (func_num_args() < 1) {
Deprecation::trigger(
'doctrine/orm',
'https://github.com/doctrine/orm/pull/9791',
'Calling %s without arguments is deprecated, pass null instead.',
__METHOD__
);
}
$this->foundMetadata = $classMetadata;
}
/** @return ClassMetadata|null */
public function getFoundMetadata()
{
return $this->foundMetadata;
}
/**
* Retrieve class name for which a failed metadata fetch attempt was executed
*
* @return string
*/
public function getClassName()
{
return $this->className;
}
}

View File

@@ -0,0 +1,73 @@
<?php
declare(strict_types=1);
namespace Doctrine\ORM\Event;
use Doctrine\Deprecations\Deprecation;
use Doctrine\ORM\EntityManagerInterface;
use Doctrine\Persistence\Event\OnClearEventArgs as BaseOnClearEventArgs;
/**
* Provides event arguments for the onClear event.
*
* @link www.doctrine-project.org
*
* @extends BaseOnClearEventArgs<EntityManagerInterface>
*/
class OnClearEventArgs extends BaseOnClearEventArgs
{
/** @var string|null */
private $entityClass;
/** @param string|null $entityClass Optional entity class. */
public function __construct(EntityManagerInterface $em, $entityClass = null)
{
parent::__construct($em);
$this->entityClass = $entityClass;
}
/**
* Retrieves associated EntityManager.
*
* @deprecated 2.13. Use {@see getObjectManager} instead.
*
* @return EntityManagerInterface
*/
public function getEntityManager()
{
Deprecation::trigger(
'doctrine/orm',
'https://github.com/doctrine/orm/issues/9875',
'Method %s() is deprecated and will be removed in Doctrine ORM 3.0. Use getObjectManager() instead.',
__METHOD__
);
return $this->getObjectManager();
}
/**
* Name of the entity class that is cleared, or empty if all are cleared.
*
* @deprecated Clearing the entity manager partially is deprecated. This method will be removed in 3.0.
*
* @return string|null
*/
public function getEntityClass()
{
return $this->entityClass;
}
/**
* Checks if event clears all entities.
*
* @deprecated Clearing the entity manager partially is deprecated. This method will be removed in 3.0.
*
* @return bool
*/
public function clearsAllEntities()
{
return $this->entityClass === null;
}
}

View File

@@ -0,0 +1,38 @@
<?php
declare(strict_types=1);
namespace Doctrine\ORM\Event;
use Doctrine\Deprecations\Deprecation;
use Doctrine\ORM\EntityManagerInterface;
use Doctrine\Persistence\Event\ManagerEventArgs;
/**
* Provides event arguments for the preFlush event.
*
* @link www.doctrine-project.org
*
* @extends ManagerEventArgs<EntityManagerInterface>
*/
class OnFlushEventArgs extends ManagerEventArgs
{
/**
* Retrieve associated EntityManager.
*
* @deprecated 2.13. Use {@see getObjectManager} instead.
*
* @return EntityManagerInterface
*/
public function getEntityManager()
{
Deprecation::trigger(
'doctrine/orm',
'https://github.com/doctrine/orm/issues/9875',
'Method %s() is deprecated and will be removed in Doctrine ORM 3.0. Use getObjectManager() instead.',
__METHOD__
);
return $this->getObjectManager();
}
}

View File

@@ -0,0 +1,38 @@
<?php
declare(strict_types=1);
namespace Doctrine\ORM\Event;
use Doctrine\Deprecations\Deprecation;
use Doctrine\ORM\EntityManagerInterface;
use Doctrine\Persistence\Event\ManagerEventArgs;
/**
* Provides event arguments for the postFlush event.
*
* @link www.doctrine-project.org
*
* @extends ManagerEventArgs<EntityManagerInterface>
*/
class PostFlushEventArgs extends ManagerEventArgs
{
/**
* Retrieves associated EntityManager.
*
* @deprecated 2.13. Use {@see getObjectManager} instead.
*
* @return EntityManagerInterface
*/
public function getEntityManager()
{
Deprecation::trigger(
'doctrine/orm',
'https://github.com/doctrine/orm/issues/9875',
'Method %s() is deprecated and will be removed in Doctrine ORM 3.0. Use getObjectManager() instead.',
__METHOD__
);
return $this->getObjectManager();
}
}

View File

@@ -0,0 +1,10 @@
<?php
declare(strict_types=1);
namespace Doctrine\ORM\Event;
/** @phpstan-ignore class.extendsDeprecatedClass */
final class PostLoadEventArgs extends LifecycleEventArgs
{
}

View File

@@ -0,0 +1,10 @@
<?php
declare(strict_types=1);
namespace Doctrine\ORM\Event;
/** @phpstan-ignore class.extendsDeprecatedClass */
final class PostPersistEventArgs extends LifecycleEventArgs
{
}

View File

@@ -0,0 +1,10 @@
<?php
declare(strict_types=1);
namespace Doctrine\ORM\Event;
/** @phpstan-ignore class.extendsDeprecatedClass */
final class PostRemoveEventArgs extends LifecycleEventArgs
{
}

View File

@@ -0,0 +1,10 @@
<?php
declare(strict_types=1);
namespace Doctrine\ORM\Event;
/** @phpstan-ignore class.extendsDeprecatedClass */
final class PostUpdateEventArgs extends LifecycleEventArgs
{
}

View File

@@ -0,0 +1,36 @@
<?php
declare(strict_types=1);
namespace Doctrine\ORM\Event;
use Doctrine\Deprecations\Deprecation;
use Doctrine\ORM\EntityManagerInterface;
use Doctrine\Persistence\Event\ManagerEventArgs;
/**
* Provides event arguments for the preFlush event.
*
* @link www.doctrine-project.com
*
* @extends ManagerEventArgs<EntityManagerInterface>
*/
class PreFlushEventArgs extends ManagerEventArgs
{
/**
* @deprecated 2.13. Use {@see getObjectManager} instead.
*
* @return EntityManagerInterface
*/
public function getEntityManager()
{
Deprecation::trigger(
'doctrine/orm',
'https://github.com/doctrine/orm/issues/9875',
'Method %s() is deprecated and will be removed in Doctrine ORM 3.0. Use getObjectManager() instead.',
__METHOD__
);
return $this->getObjectManager();
}
}

View File

@@ -0,0 +1,10 @@
<?php
declare(strict_types=1);
namespace Doctrine\ORM\Event;
/** @phpstan-ignore class.extendsDeprecatedClass */
final class PrePersistEventArgs extends LifecycleEventArgs
{
}

View File

@@ -0,0 +1,10 @@
<?php
declare(strict_types=1);
namespace Doctrine\ORM\Event;
/** @phpstan-ignore class.extendsDeprecatedClass */
final class PreRemoveEventArgs extends LifecycleEventArgs
{
}

View File

@@ -0,0 +1,118 @@
<?php
declare(strict_types=1);
namespace Doctrine\ORM\Event;
use Doctrine\ORM\EntityManagerInterface;
use Doctrine\ORM\PersistentCollection;
use InvalidArgumentException;
use function get_debug_type;
use function sprintf;
/**
* Class that holds event arguments for a preUpdate event.
*
* @phpstan-ignore class.extendsDeprecatedClass
*/
class PreUpdateEventArgs extends LifecycleEventArgs
{
/** @var array<string, array{mixed, mixed}|PersistentCollection> */
private $entityChangeSet;
/**
* @param object $entity
* @param mixed[][] $changeSet
* @psalm-param array<string, array{mixed, mixed}|PersistentCollection> $changeSet
*/
public function __construct($entity, EntityManagerInterface $em, array &$changeSet)
{
// @phpstan-ignore staticMethod.deprecatedClass
parent::__construct($entity, $em);
$this->entityChangeSet = &$changeSet;
}
/**
* Retrieves entity changeset.
*
* @return mixed[][]
* @psalm-return array<string, array{mixed, mixed}|PersistentCollection>
*/
public function getEntityChangeSet()
{
return $this->entityChangeSet;
}
/**
* Checks if field has a changeset.
*
* @param string $field
*
* @return bool
*/
public function hasChangedField($field)
{
return isset($this->entityChangeSet[$field]);
}
/**
* Gets the old value of the changeset of the changed field.
*
* @param string $field
*
* @return mixed
*/
public function getOldValue($field)
{
$this->assertValidField($field);
return $this->entityChangeSet[$field][0];
}
/**
* Gets the new value of the changeset of the changed field.
*
* @param string $field
*
* @return mixed
*/
public function getNewValue($field)
{
$this->assertValidField($field);
return $this->entityChangeSet[$field][1];
}
/**
* Sets the new value of this field.
*
* @param string $field
* @param mixed $value
*
* @return void
*/
public function setNewValue($field, $value)
{
$this->assertValidField($field);
$this->entityChangeSet[$field][1] = $value;
}
/**
* Asserts the field exists in changeset.
*
* @throws InvalidArgumentException
*/
private function assertValidField(string $field): void
{
if (! isset($this->entityChangeSet[$field])) {
throw new InvalidArgumentException(sprintf(
'Field "%s" is not a valid field of the entity "%s" in PreUpdateEventArgs.',
$field,
get_debug_type($this->getObject())
));
}
}
}

125
vendor/doctrine/orm/src/Events.php vendored Normal file
View File

@@ -0,0 +1,125 @@
<?php
declare(strict_types=1);
namespace Doctrine\ORM;
/**
* Container for all ORM events.
*
* This class cannot be instantiated.
*/
final class Events
{
/**
* Private constructor. This class is not meant to be instantiated.
*/
private function __construct()
{
}
/**
* The preRemove event occurs for a given entity before the respective
* EntityManager remove operation for that entity is executed.
*
* This is an entity lifecycle event.
*/
public const preRemove = 'preRemove';
/**
* The postRemove event occurs for an entity after the entity has
* been deleted. It will be invoked after the database delete operations.
*
* This is an entity lifecycle event.
*/
public const postRemove = 'postRemove';
/**
* The prePersist event occurs for a given entity before the respective
* EntityManager persist operation for that entity is executed.
*
* This is an entity lifecycle event.
*/
public const prePersist = 'prePersist';
/**
* The postPersist event occurs for an entity after the entity has
* been made persistent. It will be invoked after the database insert operations.
* Generated primary key values are available in the postPersist event.
*
* This is an entity lifecycle event.
*/
public const postPersist = 'postPersist';
/**
* The preUpdate event occurs before the database update operations to
* entity data.
*
* This is an entity lifecycle event.
*/
public const preUpdate = 'preUpdate';
/**
* The postUpdate event occurs after the database update operations to
* entity data.
*
* This is an entity lifecycle event.
*/
public const postUpdate = 'postUpdate';
/**
* The postLoad event occurs for an entity after the entity has been loaded
* into the current EntityManager from the database or after the refresh operation
* has been applied to it.
*
* Note that the postLoad event occurs for an entity before any associations have been
* initialized. Therefore, it is not safe to access associations in a postLoad callback
* or event handler.
*
* This is an entity lifecycle event.
*/
public const postLoad = 'postLoad';
/**
* The loadClassMetadata event occurs after the mapping metadata for a class
* has been loaded from a mapping source (attributes/xml/yaml).
*/
public const loadClassMetadata = 'loadClassMetadata';
/**
* The onClassMetadataNotFound event occurs whenever loading metadata for a class
* failed.
*/
public const onClassMetadataNotFound = 'onClassMetadataNotFound';
/**
* The preFlush event occurs when the EntityManager#flush() operation is invoked,
* but before any changes to managed entities have been calculated. This event is
* always raised right after EntityManager#flush() call.
*/
public const preFlush = 'preFlush';
/**
* The onFlush event occurs when the EntityManager#flush() operation is invoked,
* after any changes to managed entities have been determined but before any
* actual database operations are executed. The event is only raised if there is
* actually something to do for the underlying UnitOfWork. If nothing needs to be done,
* the onFlush event is not raised.
*/
public const onFlush = 'onFlush';
/**
* The postFlush event occurs when the EntityManager#flush() operation is invoked and
* after all actual database operations are executed successfully. The event is only raised if there is
* actually something to do for the underlying UnitOfWork. If nothing needs to be done,
* the postFlush event is not raised. The event won't be raised if an error occurs during the
* flush operation.
*/
public const postFlush = 'postFlush';
/**
* The onClear event occurs when the EntityManager#clear() operation is invoked,
* after all references to entities have been removed from the unit of work.
*/
public const onClear = 'onClear';
}

View File

@@ -0,0 +1,11 @@
<?php
declare(strict_types=1);
namespace Doctrine\ORM\Exception;
use Throwable;
interface ConfigurationException extends Throwable
{
}

View File

@@ -0,0 +1,42 @@
<?php
declare(strict_types=1);
namespace Doctrine\ORM\Exception;
use function get_class;
use function sprintf;
final class EntityIdentityCollisionException extends ORMException
{
/**
* @param object $existingEntity
* @param object $newEntity
*/
public static function create($existingEntity, $newEntity, string $idHash): self
{
return new self(
sprintf(
<<<'EXCEPTION'
While adding an entity of class %s with an ID hash of "%s" to the identity map,
another object of class %s was already present for the same ID. This exception
is a safeguard against an internal inconsistency - IDs should uniquely map to
entity object instances. This problem may occur if:
- you use application-provided IDs and reuse ID values;
- database-provided IDs are reassigned after truncating the database without
clearing the EntityManager;
- you might have been using EntityManager#getReference() to create a reference
for a nonexistent ID that was subsequently (by the RDBMS) assigned to another
entity.
Otherwise, it might be an ORM-internal inconsistency, please report it.
EXCEPTION
,
get_class($newEntity),
$idHash,
get_class($existingEntity)
)
);
}
}

View File

@@ -0,0 +1,13 @@
<?php
declare(strict_types=1);
namespace Doctrine\ORM\Exception;
final class EntityManagerClosed extends ORMException implements ManagerException
{
public static function create(): self
{
return new self('The EntityManager is closed.');
}
}

View File

@@ -0,0 +1,19 @@
<?php
declare(strict_types=1);
namespace Doctrine\ORM\Exception;
use function get_debug_type;
final class EntityMissingAssignedId extends ORMException
{
/** @param object $entity */
public static function forField($entity, string $field): self
{
return new self('Entity of type ' . get_debug_type($entity) . " is missing an assigned ID for field '" . $field . "'. " .
'The identifier generation strategy for this entity requires the ID field to be populated before ' .
'EntityManager#persist() is called. If you want automatically generated identifiers instead ' .
'you need to adjust the metadata mapping accordingly.');
}
}

View File

@@ -0,0 +1,17 @@
<?php
declare(strict_types=1);
namespace Doctrine\ORM\Exception;
use Doctrine\ORM\EntityRepository;
final class InvalidEntityRepository extends ORMException implements ConfigurationException
{
public static function fromClassName(string $className): self
{
return new self(
"Invalid repository class '" . $className . "'. It must be a " . EntityRepository::class . '.'
);
}
}

View File

@@ -0,0 +1,15 @@
<?php
declare(strict_types=1);
namespace Doctrine\ORM\Exception;
use function sprintf;
final class InvalidHydrationMode extends ORMException implements ManagerException
{
public static function fromMode(string $mode): self
{
return new self(sprintf('"%s" is an invalid hydration mode.', $mode));
}
}

View File

@@ -0,0 +1,11 @@
<?php
declare(strict_types=1);
namespace Doctrine\ORM\Exception;
use Throwable;
interface ManagerException extends Throwable
{
}

View File

@@ -0,0 +1,15 @@
<?php
declare(strict_types=1);
namespace Doctrine\ORM\Exception;
final class MismatchedEventManager extends ORMException implements ManagerException
{
public static function create(): self
{
return new self(
'Cannot use different EventManager instances for EntityManager and Connection.'
);
}
}

View File

@@ -0,0 +1,19 @@
<?php
declare(strict_types=1);
namespace Doctrine\ORM\Exception;
use function sprintf;
final class MissingIdentifierField extends ORMException implements ManagerException
{
public static function fromFieldAndClass(string $fieldName, string $className): self
{
return new self(sprintf(
'The identifier %s is missing for a query of %s',
$fieldName,
$className
));
}
}

View File

@@ -0,0 +1,16 @@
<?php
declare(strict_types=1);
namespace Doctrine\ORM\Exception;
final class MissingMappingDriverImplementation extends ORMException implements ManagerException
{
public static function create(): self
{
return new self(
"It's a requirement to specify a Metadata Driver and pass it " .
'to Doctrine\\ORM\\Configuration::setMetadataDriverImpl().'
);
}
}

View File

@@ -0,0 +1,24 @@
<?php
declare(strict_types=1);
namespace Doctrine\ORM\Exception;
use function implode;
use function sprintf;
final class MultipleSelectorsFoundException extends ORMException
{
public const MULTIPLE_SELECTORS_FOUND_EXCEPTION = 'Multiple selectors found: %s. Please select only one.';
/** @param string[] $selectors */
public static function create(array $selectors): self
{
return new self(
sprintf(
self::MULTIPLE_SELECTORS_FOUND_EXCEPTION,
implode(', ', $selectors)
)
);
}
}

View File

@@ -0,0 +1,18 @@
<?php
declare(strict_types=1);
namespace Doctrine\ORM\Exception;
use function sprintf;
final class NamedNativeQueryNotFound extends ORMException implements ConfigurationException
{
public static function fromName(string $name): self
{
return new self(sprintf(
'Could not find a named native query by the name "%s"',
$name
));
}
}

View File

@@ -0,0 +1,18 @@
<?php
declare(strict_types=1);
namespace Doctrine\ORM\Exception;
use function sprintf;
final class NamedQueryNotFound extends ORMException implements ConfigurationException
{
public static function fromName(string $name): self
{
return new self(sprintf(
'Could not find a named query by the name "%s"',
$name
));
}
}

View File

@@ -0,0 +1,41 @@
<?php
declare(strict_types=1);
namespace Doctrine\ORM\Exception;
use function sprintf;
final class NotSupported extends ORMException
{
public static function create(): self
{
return new self('This behaviour is (currently) not supported by Doctrine 2');
}
public static function createForDbal3(string $context): self
{
return new self(sprintf(
<<<'EXCEPTION'
Context: %s
Problem: Feature was deprecated in doctrine/dbal 2.x and is not supported by installed doctrine/dbal:3.x
Solution: See the doctrine/deprecations logs for new alternative approaches.
EXCEPTION
,
$context
));
}
public static function createForPersistence3(string $context): self
{
return new self(sprintf(
<<<'EXCEPTION'
Context: %s
Problem: Feature was deprecated in doctrine/persistence 2.x and is not supported by installed doctrine/persistence:3.x
Solution: See the doctrine/deprecations logs for new alternative approaches.
EXCEPTION
,
$context
));
}
}

View File

@@ -0,0 +1,16 @@
<?php
declare(strict_types=1);
namespace Doctrine\ORM\Exception;
use Doctrine\ORM\ORMException as BaseORMException;
/**
* Should become an interface in 3.0
*
* @phpstan-ignore class.extendsDeprecatedClass
*/
class ORMException extends BaseORMException
{
}

View File

@@ -0,0 +1,11 @@
<?php
declare(strict_types=1);
namespace Doctrine\ORM\Exception;
use Doctrine\ORM\Persisters\PersisterException as BasePersisterException;
class PersisterException extends BasePersisterException
{
}

Some files were not shown because too many files have changed in this diff Show More