Skip to content

Commit

Permalink
Implement proxy name resolver
Browse files Browse the repository at this point in the history
It is important to have the same implementation as used in
doctrine/persistence without relying on copy/paste.
  • Loading branch information
greg0ire committed Oct 12, 2023
1 parent 130a90c commit 8bebaef
Show file tree
Hide file tree
Showing 17 changed files with 88 additions and 72 deletions.
3 changes: 1 addition & 2 deletions lib/Doctrine/ORM/AbstractQuery.php
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,6 @@
use Doctrine\ORM\Cache\TimestampCacheKey;
use Doctrine\ORM\Mapping\ClassMetadata;
use Doctrine\ORM\Mapping\MappingException as ORMMappingException;
use Doctrine\ORM\Proxy\ClassUtils;
use Doctrine\ORM\Query\Parameter;
use Doctrine\ORM\Query\QueryException;
use Doctrine\ORM\Query\ResultSetMapping;
Expand Down Expand Up @@ -379,7 +378,7 @@ public function processParameterValue(mixed $value): mixed
}

try {
$class = ClassUtils::getClass($value);
$class = $this->em->getConfiguration()->getProxyClassNameResolver()->resolveClass($value);
$value = $this->em->getUnitOfWork()->getSingleIdentifierValue($value);

if ($value === null) {
Expand Down
10 changes: 6 additions & 4 deletions lib/Doctrine/ORM/Cache/DefaultCache.php
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
use Doctrine\ORM\EntityManagerInterface;
use Doctrine\ORM\Mapping\ClassMetadata;
use Doctrine\ORM\ORMInvalidArgumentException;
use Doctrine\ORM\Proxy\ClassUtils;
use Doctrine\ORM\Proxy\DefaultProxyClassNameResolver;
use Doctrine\ORM\UnitOfWork;

use function is_array;
Expand All @@ -22,6 +22,7 @@ class DefaultCache implements Cache
{
private readonly UnitOfWork $uow;
private readonly CacheFactory $cacheFactory;
private readonly DefaultProxyClassNameResolver $proxyClassNameResolver;

/**
* @var QueryCache[]
Expand All @@ -34,10 +35,11 @@ class DefaultCache implements Cache
public function __construct(
private readonly EntityManagerInterface $em,
) {
$this->uow = $em->getUnitOfWork();
$this->cacheFactory = $em->getConfiguration()
$this->uow = $em->getUnitOfWork();
$this->cacheFactory = $em->getConfiguration()
->getSecondLevelCacheConfiguration()
->getCacheFactory();
$this->proxyClassNameResolver = $em->getConfiguration()->getProxyClassNameResolver();
}

public function getEntityCacheRegion(string $className): Region|null
Expand Down Expand Up @@ -233,7 +235,7 @@ private function buildCollectionCacheKey(
private function toIdentifierArray(ClassMetadata $metadata, mixed $identifier): array
{
if (is_object($identifier)) {
$class = ClassUtils::getClass($identifier);
$class = $this->proxyClassNameResolver->resolveClass($identifier);
if ($this->em->getMetadataFactory()->hasMetadataFor($class)) {
$identifier = $this->uow->getSingleIdentifierValue($identifier)
?? throw ORMInvalidArgumentException::invalidIdentifierBindingEntity($class);
Expand Down
3 changes: 1 addition & 2 deletions lib/Doctrine/ORM/Cache/DefaultEntityHydrator.php
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@

use Doctrine\ORM\EntityManagerInterface;
use Doctrine\ORM\Mapping\ClassMetadata;
use Doctrine\ORM\Proxy\ClassUtils;
use Doctrine\ORM\Query;
use Doctrine\ORM\UnitOfWork;
use Doctrine\ORM\Utility\IdentifierFlattener;
Expand Down Expand Up @@ -97,7 +96,7 @@ public function buildCacheEntry(ClassMetadata $metadata, EntityCacheKey $key, ob
}

if (! isset($assoc->id)) {
$targetClass = ClassUtils::getClass($data[$name]);
$targetClass = $this->em->getConfiguration()->getProxyClassNameResolver()->resolveClass($data[$name]);
$targetId = $this->uow->getEntityIdentifier($data[$name]);
$data[$name] = new AssociationCacheEntry($targetClass, $targetId);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@
use Doctrine\ORM\Mapping\ClassMetadataFactory;
use Doctrine\ORM\PersistentCollection;
use Doctrine\ORM\Persisters\Collection\CollectionPersister;
use Doctrine\ORM\Proxy\ClassUtils;
use Doctrine\ORM\Proxy\DefaultProxyClassNameResolver;
use Doctrine\ORM\UnitOfWork;

use function array_values;
Expand All @@ -37,6 +37,7 @@ abstract class AbstractCollectionPersister implements CachedCollectionPersister
protected string $regionName;
protected CollectionHydrator $hydrator;
protected CacheLogger|null $cacheLogger;
protected DefaultProxyClassNameResolver $proxyClassNameResolver;

public function __construct(
protected CollectionPersister $persister,
Expand All @@ -48,13 +49,14 @@ public function __construct(
$cacheConfig = $configuration->getSecondLevelCacheConfiguration();
$cacheFactory = $cacheConfig->getCacheFactory();

$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);
$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);
$this->proxyClassNameResolver = $configuration->getProxyClassNameResolver();
}

public function getCacheRegion(): Region
Expand Down Expand Up @@ -105,7 +107,7 @@ public function storeCollectionCache(CollectionCacheKey $key, Collection|array $
}

$class = $this->targetEntity;
$className = ClassUtils::getClass($elements[$index]);
$className = $this->proxyClassNameResolver->resolveClass($elements[$index]);

if ($className !== $this->targetEntity->name) {
$class = $this->metadataFactory->getMetadataFor($className);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,15 +6,14 @@

use Doctrine\ORM\Cache\Exception\CannotUpdateReadOnlyCollection;
use Doctrine\ORM\PersistentCollection;
use Doctrine\ORM\Proxy\ClassUtils;

class ReadOnlyCachedCollectionPersister extends NonStrictReadWriteCachedCollectionPersister
{
public function update(PersistentCollection $collection): void
{
if ($collection->isDirty() && $collection->getSnapshot()) {
throw CannotUpdateReadOnlyCollection::fromEntityAndField(
ClassUtils::getClass($collection->getOwner()),
$this->proxyClassNameResolver->resolveClass($collection->getOwner()),
$this->association->fieldName,
);
}
Expand Down
24 changes: 13 additions & 11 deletions lib/Doctrine/ORM/Cache/Persister/Entity/AbstractEntityPersister.php
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@
use Doctrine\ORM\Mapping\ClassMetadataFactory;
use Doctrine\ORM\PersistentCollection;
use Doctrine\ORM\Persisters\Entity\EntityPersister;
use Doctrine\ORM\Proxy\ClassUtils;
use Doctrine\ORM\Proxy\DefaultProxyClassNameResolver;
use Doctrine\ORM\Query\ResultSetMapping;
use Doctrine\ORM\UnitOfWork;

Expand All @@ -35,6 +35,7 @@ abstract class AbstractEntityPersister implements CachedEntityPersister
{
protected UnitOfWork $uow;
protected ClassMetadataFactory $metadataFactory;
protected DefaultProxyClassNameResolver $proxyClassNameResolver;

/** @var mixed[] */
protected array $queuedCache = [];
Expand Down Expand Up @@ -63,14 +64,15 @@ public function __construct(
$cacheConfig = $configuration->getSecondLevelCacheConfiguration();
$cacheFactory = $cacheConfig->getCacheFactory();

$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);
$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);
$this->proxyClassNameResolver = $configuration->getProxyClassNameResolver();
}

public function addInsert(object $entity): void
Expand Down Expand Up @@ -147,7 +149,7 @@ public function getEntityHydrator(): EntityHydrator
public function storeEntityCache(object $entity, EntityCacheKey $key): bool
{
$class = $this->class;
$className = ClassUtils::getClass($entity);
$className = $this->proxyClassNameResolver->resolveClass($entity);

if ($className !== $this->class->name) {
$class = $this->metadataFactory->getMetadataFor($className);
Expand Down Expand Up @@ -394,7 +396,7 @@ public function loadById(array $identifier, object|null $entity = null): object|
}

$class = $this->class;
$className = ClassUtils::getClass($entity);
$className = $this->proxyClassNameResolver->resolveClass($entity);

if ($className !== $this->class->name) {
$class = $this->metadataFactory->getMetadataFor($className);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@
namespace Doctrine\ORM\Cache\Persister\Entity;

use Doctrine\ORM\Cache\Exception\CannotUpdateReadOnlyEntity;
use Doctrine\ORM\Proxy\ClassUtils;

/**
* Specific read-only region entity persister
Expand All @@ -14,6 +13,6 @@ class ReadOnlyCachedEntityPersister extends NonStrictReadWriteCachedEntityPersis
{
public function update(object $entity): void
{
throw CannotUpdateReadOnlyEntity::fromEntity(ClassUtils::getClass($entity));
throw CannotUpdateReadOnlyEntity::fromEntity($this->proxyClassNameResolver->resolveClass($entity));
}
}
15 changes: 15 additions & 0 deletions lib/Doctrine/ORM/Configuration.php
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
use Doctrine\ORM\Mapping\NamingStrategy;
use Doctrine\ORM\Mapping\QuoteStrategy;
use Doctrine\ORM\Mapping\TypedFieldMapper;
use Doctrine\ORM\Proxy\DefaultProxyClassNameResolver;
use Doctrine\ORM\Proxy\ProxyFactory;
use Doctrine\ORM\Query\AST\Functions\FunctionNode;
use Doctrine\ORM\Query\Filter\SQLFilter;
Expand Down Expand Up @@ -108,6 +109,20 @@ public function setProxyNamespace(string $ns): void
$this->attributes['proxyNamespace'] = $ns;
}

/**
* Gets the proxy class name resolver.
*
* @internal
*/
public function getProxyClassNameResolver(): DefaultProxyClassNameResolver
{
if (! isset($this->attributes['proxyClassNameResolver'])) {
$this->attributes['proxyClassNameResolver'] = new DefaultProxyClassNameResolver();
}

return $this->attributes['proxyClassNameResolver'];
}

/**
* Sets the cache driver implementation that is used for metadata caching.
*
Expand Down
3 changes: 1 addition & 2 deletions lib/Doctrine/ORM/EntityManager.php
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,6 @@
use Doctrine\ORM\Internal\Hydration\AbstractHydrator;
use Doctrine\ORM\Mapping\ClassMetadata;
use Doctrine\ORM\Mapping\ClassMetadataFactory;
use Doctrine\ORM\Proxy\ClassUtils;
use Doctrine\ORM\Proxy\ProxyFactory;
use Doctrine\ORM\Query\Expr;
use Doctrine\ORM\Query\FilterCollection;
Expand Down Expand Up @@ -281,7 +280,7 @@ public function find($className, mixed $id, LockMode|int|null $lockMode = null,

foreach ($id as $i => $value) {
if (is_object($value)) {
$className = ClassUtils::getClass($value);
$className = $this->getConfiguration()->getProxyClassNameResolver()->resolveClass($value);
if ($this->metadataFactory->hasMetadataFor($className)) {
$id[$i] = $this->unitOfWork->getSingleIdentifierValue($value);

Expand Down
2 changes: 2 additions & 0 deletions lib/Doctrine/ORM/Mapping/ClassMetadataFactory.php
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,8 @@ class ClassMetadataFactory extends AbstractClassMetadataFactory

public function setEntityManager(EntityManagerInterface $em): void
{
parent::setProxyClassNameResolver($em->getConfiguration()->getProxyClassNameResolver());

$this->em = $em;
}

Expand Down
18 changes: 10 additions & 8 deletions lib/Doctrine/ORM/Persisters/Entity/BasicEntityPersister.php
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@
use Doctrine\ORM\Persisters\Exception\UnrecognizedField;
use Doctrine\ORM\Persisters\SqlExpressionVisitor;
use Doctrine\ORM\Persisters\SqlValueVisitor;
use Doctrine\ORM\Proxy\ClassUtils;
use Doctrine\ORM\Proxy\DefaultProxyClassNameResolver;
use Doctrine\ORM\Query;
use Doctrine\ORM\Query\QueryException;
use Doctrine\ORM\Query\ResultSetMapping;
Expand Down Expand Up @@ -168,6 +168,7 @@ class BasicEntityPersister implements EntityPersister
protected CachedPersisterContext $currentPersisterContext;
private readonly CachedPersisterContext $limitsHandlingContext;
private readonly CachedPersisterContext $noLimitsContext;
private readonly DefaultProxyClassNameResolver $proxyClassNameResolver;

/**
* Initializes a new <tt>BasicEntityPersister</tt> that uses the given EntityManager
Expand All @@ -179,16 +180,17 @@ public function __construct(
protected EntityManagerInterface $em,
protected ClassMetadata $class,
) {
$this->conn = $em->getConnection();
$this->platform = $this->conn->getDatabasePlatform();
$this->quoteStrategy = $em->getConfiguration()->getQuoteStrategy();
$this->identifierFlattener = new IdentifierFlattener($em->getUnitOfWork(), $em->getMetadataFactory());
$this->noLimitsContext = $this->currentPersisterContext = new CachedPersisterContext(
$this->conn = $em->getConnection();
$this->platform = $this->conn->getDatabasePlatform();
$this->quoteStrategy = $em->getConfiguration()->getQuoteStrategy();
$this->proxyClassNameResolver = $em->getConfiguration()->getProxyClassNameResolver();
$this->identifierFlattener = new IdentifierFlattener($em->getUnitOfWork(), $em->getMetadataFactory());
$this->noLimitsContext = $this->currentPersisterContext = new CachedPersisterContext(
$class,
new Query\ResultSetMapping(),
false,
);
$this->limitsHandlingContext = new CachedPersisterContext(
$this->limitsHandlingContext = new CachedPersisterContext(
$class,
new Query\ResultSetMapping(),
true,
Expand Down Expand Up @@ -1940,7 +1942,7 @@ private function getIndividualValue(mixed $value): array
return [$value->value];
}

$valueClass = ClassUtils::getClass($value);
$valueClass = $this->proxyClassNameResolver->resolveClass($value);

if ($this->em->getMetadataFactory()->isTransient($valueClass)) {
return [$value];
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

namespace Doctrine\ORM\Proxy;

use Doctrine\Persistence\Mapping\ProxyClassNameResolver;
use Doctrine\Persistence\Proxy;

use function get_class;
Expand All @@ -16,18 +17,23 @@
*
* @internal
*/
final class ClassUtils
class DefaultProxyClassNameResolver implements ProxyClassNameResolver
{
/**
* Gets the real class name of a class name that could be a proxy.
* Gets the real class name of an object (even if its a proxy).
*
* @param class-string<Proxy<T>>|class-string<T> $className
* @param Proxy<T>|T $object
*
* @return class-string<T>
*
* @template T of object
*/
private static function getRealClass(string $className): string
public function resolveClass(object $object): string
{
return $this->resolveClassName(get_class($object));
}

public function resolveClassName(string $className): string
{
$pos = strrpos($className, '\\' . Proxy::MARKER . '\\');

Expand All @@ -37,18 +43,4 @@ private static function getRealClass(string $className): string

return substr($className, $pos + Proxy::MARKER_LENGTH + 2);
}

/**
* Gets the real class name of an object (even if its a proxy).
*
* @param Proxy<T>|T $object
*
* @return class-string<T>
*
* @template T of object
*/
public static function getClass(object $object): string
{
return self::getRealClass(get_class($object));
}
}
2 changes: 1 addition & 1 deletion phpcs.xml.dist
Original file line number Diff line number Diff line change
Expand Up @@ -204,7 +204,7 @@

<rule ref="Squiz.Classes.ValidClassName.NotCamelCaps">
<!-- we need to test what happens with an stdClass proxy -->
<exclude-pattern>tests/Doctrine/Tests/Proxy/ClassUtilsTest.php</exclude-pattern>
<exclude-pattern>tests/Doctrine/Tests/Proxy/DefaultProxyClassNameResolverTest.php</exclude-pattern>
</rule>

<rule ref="Squiz.Commenting.FunctionComment.WrongStyle">
Expand Down
4 changes: 2 additions & 2 deletions psalm-baseline.xml
Original file line number Diff line number Diff line change
Expand Up @@ -742,13 +742,13 @@
<code>require $file</code>
</UnresolvableInclude>
</file>
<file src="lib/Doctrine/ORM/Proxy/ClassUtils.php">
<file src="lib/Doctrine/ORM/Proxy/DefaultProxyClassNameResolver.php">
<LessSpecificReturnStatement>
<code>$className</code>
<code>substr($className, $pos + Proxy::MARKER_LENGTH + 2)</code>
</LessSpecificReturnStatement>
<MoreSpecificReturnType>
<code><![CDATA[class-string<T>]]></code>
<code>string</code>
</MoreSpecificReturnType>
</file>
<file src="lib/Doctrine/ORM/Proxy/ProxyFactory.php">
Expand Down
Loading

0 comments on commit 8bebaef

Please sign in to comment.