Skip to content

Commit

Permalink
Fix cloning entities when using lazy-ghost proxies
Browse files Browse the repository at this point in the history
  • Loading branch information
nicolas-grekas committed Jul 6, 2023
1 parent 0b9060c commit eee87c3
Show file tree
Hide file tree
Showing 4 changed files with 47 additions and 42 deletions.
60 changes: 35 additions & 25 deletions lib/Doctrine/ORM/Proxy/ProxyFactory.php
Original file line number Diff line number Diff line change
Expand Up @@ -48,13 +48,12 @@ class <proxyShortClassName> extends \<className> implements \<baseProxyInterface
{
<useLazyGhostTrait>
/**
* @internal
*/
public bool $__isCloning = false;
public function __construct(?\Closure $initializer = null)
public function __construct(?\Closure $initializer = null, ?\Closure $cloner = null)
{
if ($cloner !== null) {
return;
}
self::createLazyGhost($initializer, <skippedProperties>, $this);
}
Expand All @@ -63,17 +62,6 @@ public function __isInitialized(): bool
return isset($this->lazyObjectState) && $this->isLazyObjectInitialized();
}
public function __clone()
{
$this->__isCloning = true;
try {
$this->__doClone();
} finally {
$this->__isCloning = false;
}
}
public function __serialize(): array
{
<serializeImpl>
Expand All @@ -98,6 +86,9 @@ public function __serialize(): array
*/
private $identifierFlattener;

/** @var ProxyDefinition[] */
private $definitions = [];

/**
* Initializes a new instance of the <tt>ProxyFactory</tt> class that is
* connected to the given <tt>EntityManager</tt>.
Expand Down Expand Up @@ -131,6 +122,26 @@ public function __construct(EntityManagerInterface $em, $proxyDir, $proxyNs, $au
$this->identifierFlattener = new IdentifierFlattener($this->uow, $em->getMetadataFactory());
}

/**
* {@inheritDoc}
*/
public function getProxy($className, array $identifier)
{
$proxy = parent::getProxy($className, $identifier);

if (! $this->em->getConfiguration()->isLazyGhostObjectEnabled()) {
return $proxy;
}

$initializer = $this->definitions[$className]->initializer;

$proxy->__construct(static function (Proxy $object) use ($initializer, $proxy): void {
$initializer($object, $proxy);
});

return $proxy;
}

/**
* {@inheritDoc}
*/
Expand Down Expand Up @@ -158,7 +169,7 @@ protected function createProxyDefinition($className)
$cloner = $this->createCloner($classMetadata, $entityPersister);
}

return new ProxyDefinition(
return $this->definitions[$className] = new ProxyDefinition(
ClassUtils::generateProxyClassName($className, $this->proxyNs),
$classMetadata->getIdentifierFieldNames(),
$classMetadata->getReflectionProperties(),
Expand Down Expand Up @@ -231,15 +242,15 @@ private function createInitializer(ClassMetadata $classMetadata, EntityPersister
/**
* Creates a closure capable of initializing a proxy
*
* @return Closure(Proxy):void
* @return Closure(Proxy, Proxy):void
*
* @throws EntityNotFoundException
*/
private function createLazyInitializer(ClassMetadata $classMetadata, EntityPersister $entityPersister): Closure
{
return function (Proxy $proxy) use ($entityPersister, $classMetadata): void {
$identifier = $classMetadata->getIdentifierValues($proxy);
$entity = $entityPersister->loadById($identifier, $proxy->__isCloning ? null : $proxy);
return function (Proxy $proxy, Proxy $original) use ($entityPersister, $classMetadata): void {
$identifier = $classMetadata->getIdentifierValues($original);
$entity = $entityPersister->loadById($identifier, $original);

if ($entity === null) {
throw EntityNotFoundException::fromClassNameAndIdentifier(
Expand All @@ -248,7 +259,7 @@ private function createLazyInitializer(ClassMetadata $classMetadata, EntityPersi
);
}

if (! $proxy->__isCloning) {
if ($proxy === $original) {
return;
}

Expand Down Expand Up @@ -315,15 +326,14 @@ private function generateUseLazyGhostTrait(ClassMetadata $class): string
isLazyObjectInitialized as private;
createLazyGhost as private;
resetLazyObject as private;
__clone as private __doClone;
}'), $code);

return $code;
}

private function generateSkippedProperties(ClassMetadata $class): string
{
$skippedProperties = ['__isCloning' => true];
$skippedProperties = [];
$identifiers = array_flip($class->getIdentifierFieldNames());
$filter = ReflectionProperty::IS_PUBLIC | ReflectionProperty::IS_PROTECTED | ReflectionProperty::IS_PRIVATE;
$reflector = $class->getReflectionClass();
Expand Down
2 changes: 1 addition & 1 deletion phpstan-baseline.neon
Original file line number Diff line number Diff line change
Expand Up @@ -286,7 +286,7 @@ parameters:
path: lib/Doctrine/ORM/Proxy/ProxyFactory.php

-
message: "#^Access to an undefined property Doctrine\\\\Persistence\\\\Proxy\\:\\:\\$__isCloning\\.$#"
message: "#^Call to an undefined method Doctrine\\\\Common\\\\Proxy\\\\Proxy\\:\\:__construct\\(\\)\\.$#"
count: 1
path: lib/Doctrine/ORM/Proxy/ProxyFactory.php

Expand Down
7 changes: 6 additions & 1 deletion psalm-baseline.xml
Original file line number Diff line number Diff line change
Expand Up @@ -1392,6 +1392,11 @@
<code>$classMetadata</code>
<code>$classMetadata</code>
</ArgumentTypeCoercion>
<DirectConstructorCall>
<code><![CDATA[$proxy->__construct(static function (Proxy $object) use ($initializer, $proxy): void {
$initializer($object, $proxy);
})]]></code>
</DirectConstructorCall>
<InvalidArgument>
<code><![CDATA[$classMetadata->getReflectionProperties()]]></code>
<code><![CDATA[$em->getMetadataFactory()]]></code>
Expand All @@ -1400,7 +1405,6 @@
<NoInterfaceProperties>
<code><![CDATA[$metadata->isEmbeddedClass]]></code>
<code><![CDATA[$metadata->isMappedSuperclass]]></code>
<code><![CDATA[$proxy->__isCloning]]></code>
</NoInterfaceProperties>
<PossiblyNullPropertyFetch>
<code><![CDATA[$property->name]]></code>
Expand All @@ -1411,6 +1415,7 @@
<code>setAccessible</code>
</PossiblyNullReference>
<UndefinedInterfaceMethod>
<code>__construct</code>
<code>__wakeup</code>
</UndefinedInterfaceMethod>
</file>
Expand Down
20 changes: 5 additions & 15 deletions tests/Doctrine/Tests/ORM/Proxy/ProxyFactoryTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@
namespace Doctrine\Tests\ORM\Proxy;

use Doctrine\Common\EventManager;
use Doctrine\Common\Proxy\Proxy as CommonProxy;
use Doctrine\DBAL\Connection;
use Doctrine\DBAL\Platforms\AbstractPlatform;
use Doctrine\ORM\EntityNotFoundException;
Expand Down Expand Up @@ -227,21 +226,12 @@ public function testProxyClonesParentFields(): void
->expects(self::atLeastOnce())
->method('loadById');

if ($proxy instanceof CommonProxy) {
$loadByIdMock->willReturn($companyEmployee);
$loadByIdMock->willReturn($companyEmployee);

$persister
->expects(self::atLeastOnce())
->method('getClassMetadata')
->willReturn($classMetaData);
} else {
$loadByIdMock->willReturnCallback(static function (array $id, CompanyEmployee $companyEmployee) {
$companyEmployee->setSalary(1000); // A property on the CompanyEmployee
$companyEmployee->setName('Bob'); // A property on the parent class, CompanyPerson

return $companyEmployee;
});
}
$persister
->expects(self::atLeastOnce())
->method('getClassMetadata')
->willReturn($classMetaData);

$cloned = clone $proxy;
assert($cloned instanceof CompanyEmployee);
Expand Down

0 comments on commit eee87c3

Please sign in to comment.