Skip to content

Commit

Permalink
Fix id hash of entity with enum as identifier
Browse files Browse the repository at this point in the history
When an entity have a backed enum as identifier, `UnitOfWork` tries to
cast to string when generating the hash of the id.
This fix calls `->value` when identifier is a `BackedEnum`.
Fixes #10471
Fixes #10334
  • Loading branch information
Gwemox committed Apr 3, 2023
1 parent 31ff969 commit 09b4a75
Show file tree
Hide file tree
Showing 9 changed files with 353 additions and 21 deletions.
19 changes: 12 additions & 7 deletions lib/Doctrine/ORM/Internal/Hydration/ObjectHydrator.php
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@

use function array_fill_keys;
use function array_keys;
use function array_map;
use function count;
use function is_array;
use function key;
Expand Down Expand Up @@ -284,13 +285,17 @@ private function getEntityFromIdentityMap(string $className, array $data)
$class = $this->_metadataCache[$className];

if ($class->isIdentifierComposite) {
$idHash = '';

foreach ($class->identifier as $fieldName) {
$idHash .= ' ' . (isset($class->associationMappings[$fieldName])
? $data[$class->associationMappings[$fieldName]['joinColumns'][0]['name']]
: $data[$fieldName]);
}
$idHash = UnitOfWork::getIdHashByIdentifier(
array_map(
/** @return mixed */
static function (string $fieldName) use ($data, $class) {
return isset($class->associationMappings[$fieldName])
? $data[$class->associationMappings[$fieldName]['joinColumns'][0]['name']]
: $data[$fieldName];
},
$class->identifier
)
);

return $this->_uow->tryGetByIdHash(ltrim($idHash), $class->rootEntityName);
} elseif (isset($class->associationMappings[$class->identifier[0]])) {
Expand Down
4 changes: 4 additions & 0 deletions lib/Doctrine/ORM/Persisters/Entity/BasicEntityPersister.php
Original file line number Diff line number Diff line change
Expand Up @@ -271,6 +271,10 @@ public function executeInserts()
$paramIndex = 1;

foreach ($insertData[$tableName] as $column => $value) {
if ($value instanceof BackedEnum) {
$value = $value->value;
}

$stmt->bindValue($paramIndex++, $value, $this->columnTypes[$column]);
}
}
Expand Down
66 changes: 52 additions & 14 deletions lib/Doctrine/ORM/UnitOfWork.php
Original file line number Diff line number Diff line change
Expand Up @@ -1566,14 +1566,8 @@ public function isEntityScheduled($entity)
public function addToIdentityMap($entity)
{
$classMetadata = $this->em->getClassMetadata(get_class($entity));
$identifier = $this->entityIdentifiers[spl_object_id($entity)];

if (empty($identifier) || in_array(null, $identifier, true)) {
throw ORMInvalidArgumentException::entityWithoutIdentity($classMetadata->name, $entity);
}

$idHash = implode(' ', $identifier);
$className = $classMetadata->rootEntityName;
$idHash = $this->getIdHashByEntity($entity);
$className = $classMetadata->rootEntityName;

if (isset($this->identityMap[$className][$idHash])) {
return false;
Expand All @@ -1584,6 +1578,50 @@ public function addToIdentityMap($entity)
return true;
}

/**
* Gets the id hash of an entity by its identifier.
*
* @param array<string|int, mixed> $identifier The identifier of an entity
*
* @return string The entity id hash.
*/
final public static function getIdHashByIdentifier(array $identifier): string
{
return implode(
' ',
array_map(
static function ($value) {
if ($value instanceof BackedEnum) {
return $value->value;
}

return $value;
},
$identifier
)
);
}

/**
* Gets the id hash of an entity.
*
* @param object $entity The entity managed by Unit Of Work
*
* @return string The entity id hash.
*/
public function getIdHashByEntity($entity): string
{
$identifier = $this->entityIdentifiers[spl_object_id($entity)];

if (empty($identifier) || in_array(null, $identifier, true)) {
$classMetadata = $this->em->getClassMetadata(get_class($entity));

throw ORMInvalidArgumentException::entityWithoutIdentity($classMetadata->name, $entity);
}

return self::getIdHashByIdentifier($identifier);
}

/**
* Gets the state of an entity with regard to the current unit of work.
*
Expand Down Expand Up @@ -1686,7 +1724,7 @@ public function removeFromIdentityMap($entity)
{
$oid = spl_object_id($entity);
$classMetadata = $this->em->getClassMetadata(get_class($entity));
$idHash = implode(' ', $this->entityIdentifiers[$oid]);
$idHash = self::getIdHashByIdentifier($this->entityIdentifiers[$oid]);

if ($idHash === '') {
throw ORMInvalidArgumentException::entityHasNoIdentity($entity, 'remove from identity map');
Expand Down Expand Up @@ -1756,7 +1794,7 @@ public function isInIdentityMap($entity)
}

$classMetadata = $this->em->getClassMetadata(get_class($entity));
$idHash = implode(' ', $this->entityIdentifiers[$oid]);
$idHash = self::getIdHashByIdentifier($this->entityIdentifiers[$oid]);

return isset($this->identityMap[$classMetadata->rootEntityName][$idHash]);
}
Expand Down Expand Up @@ -2713,7 +2751,7 @@ public function createEntity($className, array $data, &$hints = [])
$class = $this->em->getClassMetadata($className);

$id = $this->identifierFlattener->flattenIdentifier($class, $data);
$idHash = implode(' ', $id);
$idHash = self::getIdHashByIdentifier($id);

if (isset($this->identityMap[$class->rootEntityName][$idHash])) {
$entity = $this->identityMap[$class->rootEntityName][$idHash];
Expand Down Expand Up @@ -2872,7 +2910,7 @@ public function createEntity($className, array $data, &$hints = [])
// Check identity map first
// FIXME: Can break easily with composite keys if join column values are in
// wrong order. The correct order is the one in ClassMetadata#identifier.
$relatedIdHash = implode(' ', $associatedId);
$relatedIdHash = self::getIdHashByIdentifier($associatedId);

switch (true) {
case isset($this->identityMap[$targetClass->rootEntityName][$relatedIdHash]):
Expand Down Expand Up @@ -3157,7 +3195,7 @@ public function getSingleIdentifierValue($entity)
*/
public function tryGetById($id, $rootClassName)
{
$idHash = implode(' ', (array) $id);
$idHash = self::getIdHashByIdentifier((array) $id);

return $this->identityMap[$rootClassName][$idHash] ?? false;
}
Expand Down Expand Up @@ -3539,7 +3577,7 @@ private function isIdentifierEquals($entity1, $entity2): bool
$id1 = $this->entityIdentifiers[$oid1] ?? $this->identifierFlattener->flattenIdentifier($class, $class->getIdentifierValues($entity1));
$id2 = $this->entityIdentifiers[$oid2] ?? $this->identifierFlattener->flattenIdentifier($class, $class->getIdentifierValues($entity2));

return $id1 === $id2 || implode(' ', $id1) === implode(' ', $id2);
return $id1 === $id2 || self::getIdHashByIdentifier($id1) === self::getIdHashByIdentifier($id2);
}

/** @throws ORMInvalidArgumentException */
Expand Down
40 changes: 40 additions & 0 deletions tests/Doctrine/Tests/Models/GH10334/GH10334Foo.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
<?php

declare(strict_types=1);

namespace Doctrine\Tests\Models\GH10334;

use Doctrine\ORM\Mapping\Column;
use Doctrine\ORM\Mapping\Entity;
use Doctrine\ORM\Mapping\GeneratedValue;
use Doctrine\ORM\Mapping\Id;
use Doctrine\ORM\Mapping\JoinColumn;
use Doctrine\ORM\Mapping\ManyToOne;

/**
* @Entity
*/
class GH10334Foo
{
/**
* @var GH10334FooCollection
* @Id
* @ManyToOne(targetEntity="GH10334FooCollection", inversedBy="foos")
* @JoinColumn(name="foo_collection_id", referencedColumnName="id", nullable = false)
* @GeneratedValue
*/
protected $collection;

/**
* @var GH10334ProductTypeId
* @Id
* @Column(type="string", enumType="Doctrine\Tests\Models\GH10334\GH10334ProductTypeId")
*/
protected $productTypeId;

public function __construct(GH10334FooCollection $collection, GH10334ProductTypeId $productTypeId)
{
$this->collection = $collection;
$this->productTypeId = $productTypeId;
}
}
46 changes: 46 additions & 0 deletions tests/Doctrine/Tests/Models/GH10334/GH10334FooCollection.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
<?php

declare(strict_types=1);

namespace Doctrine\Tests\Models\GH10334;

use Doctrine\Common\Collections\ArrayCollection;
use Doctrine\Common\Collections\Collection;
use Doctrine\ORM\Mapping\Column;
use Doctrine\ORM\Mapping\Entity;
use Doctrine\ORM\Mapping\GeneratedValue;
use Doctrine\ORM\Mapping\Id;
use Doctrine\ORM\Mapping\OneToMany;

/**
* @Entity
*/
class GH10334FooCollection
{
/**
* @var int
* @Id
* @Column(type="integer")
* @GeneratedValue
*/
protected $id;

/**
* @OneToMany(targetEntity="GH10334Foo", mappedBy="collection", cascade={"persist", "remove"})
* @var Collection<GH10334Foo> $foos
*/
private $foos;

public function __construct()
{
$this->foos = new ArrayCollection();
}

/**
* @return Collection<GH10334Foo>
*/
public function getFoos(): Collection
{
return $this->foos;
}
}
55 changes: 55 additions & 0 deletions tests/Doctrine/Tests/Models/GH10334/GH10334Product.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
<?php

declare(strict_types=1);

namespace Doctrine\Tests\Models\GH10334;

use Doctrine\ORM\Mapping\Column;
use Doctrine\ORM\Mapping\Entity;
use Doctrine\ORM\Mapping\GeneratedValue;
use Doctrine\ORM\Mapping\Id;
use Doctrine\ORM\Mapping\JoinColumn;
use Doctrine\ORM\Mapping\ManyToOne;

/**
* @Entity
*/
class GH10334Product
{
/**
* @var int
* @Id
* @Column(name="product_id", type="integer")
* @GeneratedValue()
*/
protected $id;

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

/**
* @var GH10334ProductType $productType
* @ManyToOne(targetEntity="GH10334ProductType", inversedBy="products")
* @JoinColumn(name="product_type_id", referencedColumnName="id", nullable = false)
*/
private $productType;

public function __construct(string $name, GH10334ProductType $productType)
{
$this->name = $name;
$this->productType = $productType;
}

public function getProductType(): GH10334ProductType
{
return $this->productType;
}

public function setProductType(GH10334ProductType $productType): void
{
$this->productType = $productType;
}
}
55 changes: 55 additions & 0 deletions tests/Doctrine/Tests/Models/GH10334/GH10334ProductType.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
<?php

declare(strict_types=1);

namespace Doctrine\Tests\Models\GH10334;

use Doctrine\Common\Collections\ArrayCollection;
use Doctrine\Common\Collections\Collection;
use Doctrine\ORM\Mapping\Column;
use Doctrine\ORM\Mapping\Entity;
use Doctrine\ORM\Mapping\Id;
use Doctrine\ORM\Mapping\OneToMany;

/**
* @Entity
*/
class GH10334ProductType
{
/**
* @var GH10334ProductTypeId
* @Id
* @Column(type="string", enumType="Doctrine\Tests\Models\GH10334\GH10334ProductTypeId")
*/
protected $id;

/**
* @var float
* @Column(type="float")
*/
private $value;

/**
* @OneToMany(targetEntity="GH10334Product", mappedBy="productType", cascade={"persist", "remove"})
* @var Collection $products
*/
private $products;

public function __construct(GH10334ProductTypeId $id, float $value)
{
$this->id = $id;
$this->value = $value;
$this->products = new ArrayCollection();
}

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

public function addProduct(GH10334Product $product): void
{
$product->setProductType($this);
$this->products->add($product);
}
}
11 changes: 11 additions & 0 deletions tests/Doctrine/Tests/Models/GH10334/GH10334ProductTypeId.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
<?php

declare(strict_types=1);

namespace Doctrine\Tests\Models\GH10334;

enum GH10334ProductTypeId: string
{
case Jean = 'jean';
case Short = 'short';
}
Loading

0 comments on commit 09b4a75

Please sign in to comment.