Skip to content

Commit

Permalink
Fix cloning entities
Browse files Browse the repository at this point in the history
  • Loading branch information
nicolas-grekas committed May 27, 2024
1 parent 1a5a4c6 commit 20cc649
Show file tree
Hide file tree
Showing 5 changed files with 89 additions and 8 deletions.
4 changes: 3 additions & 1 deletion psalm-baseline.xml
Original file line number Diff line number Diff line change
Expand Up @@ -1502,7 +1502,9 @@
<code><![CDATA[__wakeup]]></code>
</UndefinedInterfaceMethod>
<UndefinedMethod>
<code><![CDATA[self::createLazyGhost($initializer, $skippedProperties)]]></code>
<code><![CDATA[self::createLazyGhost(static function (InternalProxy $object) use ($initializer, $identifier): void {
$initializer($object, $identifier);
}, $skippedProperties)]]></code>
</UndefinedMethod>
<UnresolvableInclude>
<code><![CDATA[require $fileName]]></code>
Expand Down
13 changes: 7 additions & 6 deletions src/Proxy/ProxyFactory.php
Original file line number Diff line number Diff line change
Expand Up @@ -354,15 +354,14 @@ private function createInitializer(ClassMetadata $classMetadata, EntityPersister
/**
* Creates a closure capable of initializing a proxy
*
* @return Closure(InternalProxy, InternalProxy):void
* @return Closure(InternalProxy, array):void
*
* @throws EntityNotFoundException
*/
private function createLazyInitializer(ClassMetadata $classMetadata, EntityPersister $entityPersister, IdentifierFlattener $identifierFlattener): Closure
{
return static function (InternalProxy $proxy) use ($entityPersister, $classMetadata, $identifierFlattener): void {
$identifier = $classMetadata->getIdentifierValues($proxy);
$original = $entityPersister->loadById($identifier);
return static function (InternalProxy $proxy, array $identifier) use ($entityPersister, $classMetadata, $identifierFlattener): void {
$original = $entityPersister->loadById($identifier);

if ($original === null) {
throw EntityNotFoundException::fromClassNameAndIdentifier(
Expand All @@ -378,7 +377,7 @@ private function createLazyInitializer(ClassMetadata $classMetadata, EntityPersi
$class = $entityPersister->getClassMetadata();

foreach ($class->getReflectionProperties() as $property) {
if (! $class->hasField($property->name) && ! $class->hasAssociation($property->name)) {
if (isset($identifier[$property->name]) || ! $class->hasField($property->name) && ! $class->hasAssociation($property->name)) {
continue;
}

Expand Down Expand Up @@ -468,7 +467,9 @@ private function getProxyFactory(string $className): Closure
$identifierFields = array_intersect_key($class->getReflectionProperties(), $identifiers);

$proxyFactory = Closure::bind(static function (array $identifier) use ($initializer, $skippedProperties, $identifierFields, $className): InternalProxy {
$proxy = self::createLazyGhost($initializer, $skippedProperties);
$proxy = self::createLazyGhost(static function (InternalProxy $object) use ($initializer, $identifier): void {
$initializer($object, $identifier);
}, $skippedProperties);

foreach ($identifierFields as $idField => $reflector) {
if (! isset($identifier[$idField])) {
Expand Down
59 changes: 59 additions & 0 deletions tests/Tests/Models/ECommerce/ECommerceProduct2.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
<?php

declare(strict_types=1);

namespace Doctrine\Tests\Models\ECommerce;

use Doctrine\Common\Collections\ArrayCollection;

Check failure on line 7 in tests/Tests/Models/ECommerce/ECommerceProduct2.php

View workflow job for this annotation

GitHub Actions / coding-standards / Coding Standards (8.3)

Type Doctrine\Common\Collections\ArrayCollection is not used in this file.
use Doctrine\Common\Collections\Collection;

Check failure on line 8 in tests/Tests/Models/ECommerce/ECommerceProduct2.php

View workflow job for this annotation

GitHub Actions / coding-standards / Coding Standards (8.3)

Type Doctrine\Common\Collections\Collection is not used in this file.
use Doctrine\ORM\Mapping\Column;
use Doctrine\ORM\Mapping\Entity;
use Doctrine\ORM\Mapping\GeneratedValue;
use Doctrine\ORM\Mapping\Id;
use Doctrine\ORM\Mapping\Index;
use Doctrine\ORM\Mapping\JoinColumn;

Check failure on line 14 in tests/Tests/Models/ECommerce/ECommerceProduct2.php

View workflow job for this annotation

GitHub Actions / coding-standards / Coding Standards (8.3)

Type Doctrine\ORM\Mapping\JoinColumn is not used in this file.
use Doctrine\ORM\Mapping\JoinTable;

Check failure on line 15 in tests/Tests/Models/ECommerce/ECommerceProduct2.php

View workflow job for this annotation

GitHub Actions / coding-standards / Coding Standards (8.3)

Type Doctrine\ORM\Mapping\JoinTable is not used in this file.
use Doctrine\ORM\Mapping\ManyToMany;

Check failure on line 16 in tests/Tests/Models/ECommerce/ECommerceProduct2.php

View workflow job for this annotation

GitHub Actions / coding-standards / Coding Standards (8.3)

Type Doctrine\ORM\Mapping\ManyToMany is not used in this file.
use Doctrine\ORM\Mapping\OneToMany;

Check failure on line 17 in tests/Tests/Models/ECommerce/ECommerceProduct2.php

View workflow job for this annotation

GitHub Actions / coding-standards / Coding Standards (8.3)

Type Doctrine\ORM\Mapping\OneToMany is not used in this file.
use Doctrine\ORM\Mapping\OneToOne;

Check failure on line 18 in tests/Tests/Models/ECommerce/ECommerceProduct2.php

View workflow job for this annotation

GitHub Actions / coding-standards / Coding Standards (8.3)

Type Doctrine\ORM\Mapping\OneToOne is not used in this file.
use Doctrine\ORM\Mapping\Table;

/**
* ECommerceProduct2
* Resets the id when being cloned.
*
* @Entity
* @Table(name="ecommerce_products",indexes={@Index(name="name_idx", columns={"name"})})
*/
class ECommerceProduct2
{
/**
* @var int|null
* @Column(type="integer")
* @Id
* @GeneratedValue
*/
private $id;

/**
* @var string
* @Column(type="string", length=50, nullable=true)
*/
private $name;

public function getId(): ?int
{
return $this->id;
}

public function getName(): string
{
return $this->name;
}

public function __clone()
{
$this->id = null;

Check failure on line 56 in tests/Tests/Models/ECommerce/ECommerceProduct2.php

View workflow job for this annotation

GitHub Actions / coding-standards / Coding Standards (8.3)

Equals sign not aligned with surrounding assignments; expected 3 spaces but found 1 space
$this->name = 'Clone of ' . $this->name;
}
}
2 changes: 1 addition & 1 deletion tests/Tests/ORM/Functional/ProxiesLikeEntitiesTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,7 @@ protected function setUp(): void
public function testPersistUpdate(): void
{
// Considering case (a)
$proxy = $this->_em->getProxyFactory()->getProxy(CmsUser::class, ['id' => 123]);
$proxy = $this->_em->getProxyFactory()->getProxy(CmsUser::class, ['id' => $this->user->getId()]);

$proxy->id = null;
$proxy->username = 'ocra';
Expand Down
19 changes: 19 additions & 0 deletions tests/Tests/ORM/Functional/ReferenceProxyTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
use Doctrine\ORM\Proxy\InternalProxy;
use Doctrine\Tests\Models\Company\CompanyAuction;
use Doctrine\Tests\Models\ECommerce\ECommerceProduct;
use Doctrine\Tests\Models\ECommerce\ECommerceProduct2;
use Doctrine\Tests\Models\ECommerce\ECommerceShipping;
use Doctrine\Tests\OrmFunctionalTestCase;

Expand Down Expand Up @@ -112,6 +113,24 @@ public function testCloneProxy(): void
self::assertFalse($entity->isCloned);
}

public function testCloneProxyWithResetId(): void
{
$id = $this->createProduct();

$entity = $this->_em->getReference(ECommerceProduct2::class, $id);
assert($entity instanceof ECommerceProduct2);

$clone = clone $entity;
assert($clone instanceof ECommerceProduct2);

self::assertEquals($id, $entity->getId());
self::assertEquals('Doctrine Cookbook', $entity->getName());

self::assertFalse($this->_em->contains($clone));
self::assertNull($clone->getId());
self::assertEquals('Clone of Doctrine Cookbook', $clone->getName());
}

/** @group DDC-733 */
public function testInitializeProxy(): void
{
Expand Down

0 comments on commit 20cc649

Please sign in to comment.