From 0ef8084e5c991c28cebbef9eb4b6d79bb88d4169 Mon Sep 17 00:00:00 2001 From: Tomas Date: Sun, 11 Dec 2022 17:18:59 +0100 Subject: [PATCH] Allow enum discriminator columns In all hydrators, discriminator columns are for all purposes converted to string using explicit (string) conversion. This is not allowed with PHP enums. Therefore, I added code which checks whether the variable is a BackedEnum instance and if so, instead its ->value is used (which can be cast to string without issues as it's a scalar) --- .../Internal/Hydration/AbstractHydrator.php | 28 +++- .../ORM/Internal/Hydration/ObjectHydrator.php | 8 +- .../Hydration/SimpleObjectHydrator.php | 5 + .../Tests/Models/GH10288/GH10288People.php | 11 ++ .../ORM/Functional/Ticket/GH10288Test.php | 152 ++++++++++++++++++ 5 files changed, 196 insertions(+), 8 deletions(-) create mode 100644 tests/Doctrine/Tests/Models/GH10288/GH10288People.php create mode 100644 tests/Doctrine/Tests/ORM/Functional/Ticket/GH10288Test.php diff --git a/lib/Doctrine/ORM/Internal/Hydration/AbstractHydrator.php b/lib/Doctrine/ORM/Internal/Hydration/AbstractHydrator.php index 5af8b876691..51e38594b60 100644 --- a/lib/Doctrine/ORM/Internal/Hydration/AbstractHydrator.php +++ b/lib/Doctrine/ORM/Internal/Hydration/AbstractHydrator.php @@ -450,11 +450,15 @@ protected function gatherRowData(array $data, array &$id, array &$nonemptyCompon // If there are field name collisions in the child class, then we need // to only hydrate if we are looking at the correct discriminator value - if ( - isset($cacheKeyInfo['discriminatorColumn'], $data[$cacheKeyInfo['discriminatorColumn']]) - && ! in_array((string) $data[$cacheKeyInfo['discriminatorColumn']], $cacheKeyInfo['discriminatorValues'], true) - ) { - break; + if (isset($cacheKeyInfo['discriminatorColumn'], $data[$cacheKeyInfo['discriminatorColumn']])) { + $discriminatorValue = $data[$cacheKeyInfo['discriminatorColumn']]; + if ($discriminatorValue instanceof BackedEnum) { + $discriminatorValue = $discriminatorValue->value; + } + + if (! in_array((string) $discriminatorValue, $cacheKeyInfo['discriminatorValues'], true)) { + break; + } } // in an inheritance hierarchy the same field could be defined several times. @@ -633,12 +637,22 @@ private function getDiscriminatorValues(ClassMetadata $classMetadata): array { $values = array_map( function (string $subClass): string { - return (string) $this->getClassMetadata($subClass)->discriminatorValue; + $discriminatorValue = $this->getClassMetadata($subClass)->discriminatorValue; + if ($discriminatorValue instanceof BackedEnum) { + $discriminatorValue = $discriminatorValue->value; + } + + return (string) $discriminatorValue; }, $classMetadata->subClasses ); - $values[] = (string) $classMetadata->discriminatorValue; + $discriminatorValue = $classMetadata->discriminatorValue; + if ($discriminatorValue instanceof BackedEnum) { + $discriminatorValue = $discriminatorValue->value; + } + + $values[] = (string) $discriminatorValue; return $values; } diff --git a/lib/Doctrine/ORM/Internal/Hydration/ObjectHydrator.php b/lib/Doctrine/ORM/Internal/Hydration/ObjectHydrator.php index 20865f10812..42962234426 100644 --- a/lib/Doctrine/ORM/Internal/Hydration/ObjectHydrator.php +++ b/lib/Doctrine/ORM/Internal/Hydration/ObjectHydrator.php @@ -4,6 +4,7 @@ namespace Doctrine\ORM\Internal\Hydration; +use BackedEnum; use Doctrine\Common\Collections\ArrayCollection; use Doctrine\Common\Proxy\Proxy; use Doctrine\ORM\Mapping\ClassMetadata; @@ -246,7 +247,12 @@ private function getEntity(array $data, string $dqlAlias) } $discrMap = $this->_metadataCache[$className]->discriminatorMap; - $discriminatorValue = (string) $data[$discrColumn]; + $discriminatorValue = $data[$discrColumn]; + if ($discriminatorValue instanceof BackedEnum) { + $discriminatorValue = $discriminatorValue->value; + } + + $discriminatorValue = (string) $discriminatorValue; if (! isset($discrMap[$discriminatorValue])) { throw HydrationException::invalidDiscriminatorValue($discriminatorValue, array_keys($discrMap)); diff --git a/lib/Doctrine/ORM/Internal/Hydration/SimpleObjectHydrator.php b/lib/Doctrine/ORM/Internal/Hydration/SimpleObjectHydrator.php index 454330290eb..5ad8fc04675 100644 --- a/lib/Doctrine/ORM/Internal/Hydration/SimpleObjectHydrator.php +++ b/lib/Doctrine/ORM/Internal/Hydration/SimpleObjectHydrator.php @@ -4,6 +4,7 @@ namespace Doctrine\ORM\Internal\Hydration; +use BackedEnum; use Doctrine\ORM\Internal\SQLResultCasing; use Doctrine\ORM\Mapping\ClassMetadata; use Doctrine\ORM\Query; @@ -111,6 +112,10 @@ protected function hydrateRowData(array $row, array &$result) $entityName = $discrMap[$row[$discrColumnName]]; $discrColumnValue = $row[$discrColumnName]; + if ($discrColumnValue instanceof BackedEnum) { + $discrColumnValue = $discrColumnValue->value; + } + unset($row[$discrColumnName]); } diff --git a/tests/Doctrine/Tests/Models/GH10288/GH10288People.php b/tests/Doctrine/Tests/Models/GH10288/GH10288People.php new file mode 100644 index 00000000000..c5d8c4e8454 --- /dev/null +++ b/tests/Doctrine/Tests/Models/GH10288/GH10288People.php @@ -0,0 +1,11 @@ +createSchemaForModels( + GH10288Person::class, + GH10288Boss::class, + GH10288Employee::class + ); + } + + /** + * The intent of this test is to ensure that the ORM is capable + * of using objects as discriminators (which makes things a bit + * more dynamic as you can see on the mapping of `GH10288Person`) + * + * @group GH-6141 + */ + public function testEnumDiscriminatorsShouldBeConvertedToString(): void + { + $boss = new GH10288Boss('John'); + $employee = new GH10288Employee('Bob'); + + $this->_em->persist($boss); + $this->_em->persist($employee); + $this->_em->flush(); + $this->_em->clear(); + + // Using DQL here to make sure that we'll use ObjectHydrator instead of SimpleObjectHydrator + $query = $this->_em->createQueryBuilder() + ->select('person') + ->from(GH10288Person::class, 'person') + ->where('person.name = :name') + ->setMaxResults(1) + ->getQuery(); + + $query->setParameter('name', 'John'); + self::assertEquals($boss, $query->getOneOrNullResult(AbstractQuery::HYDRATE_OBJECT)); + self::assertEquals( + GH10288People::BOSS, + $query->getOneOrNullResult(AbstractQuery::HYDRATE_ARRAY)['discr'] + ); + + $query->setParameter('name', 'Bob'); + self::assertEquals($employee, $query->getOneOrNullResult(AbstractQuery::HYDRATE_OBJECT)); + self::assertEquals( + GH10288People::EMPLOYEE, + $query->getOneOrNullResult(AbstractQuery::HYDRATE_ARRAY)['discr'] + ); + } +} + +class GH10288PeopleType extends StringType +{ + public const NAME = 'GH10288people'; + + /** + * {@inheritdoc} + */ + public function convertToDatabaseValue($value, AbstractPlatform $platform) + { + if (! $value instanceof GH10288People) { + $value = GH10288People::from($value); + } + + return $value->value; + } + + /** + * {@inheritdoc} + */ + public function convertToPHPValue($value, AbstractPlatform $platform) + { + return GH10288People::from($value); + } + + /** + * {@inheritdoc} + */ + public function getName() + { + return self::NAME; + } +} + +/** + * @Entity + * @InheritanceType("JOINED") + * @DiscriminatorColumn(name="discr", type="GH10288people") + * @DiscriminatorMap({ + * "boss" = GH10288Boss::class, + * "employee" = GH10288Employee::class + * }) + */ +abstract class GH10288Person +{ + /** + * @var int + * @Id + * @Column(type="integer") + * @GeneratedValue(strategy="AUTO") + */ + public $id; + + /** + * @var string + * @Column(type="string", length=255) + */ + public $name; + + public function __construct(string $name) + { + $this->name = $name; + } +} + +/** @Entity */ +class GH10288Boss extends GH10288Person +{ +} + +/** @Entity */ +class GH10288Employee extends GH10288Person +{ +}