Skip to content

Commit

Permalink
Allow enum discriminator columns
Browse files Browse the repository at this point in the history
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)
  • Loading branch information
michnovka committed Dec 11, 2022
1 parent b25561a commit 0ef8084
Show file tree
Hide file tree
Showing 5 changed files with 196 additions and 8 deletions.
28 changes: 21 additions & 7 deletions lib/Doctrine/ORM/Internal/Hydration/AbstractHydrator.php
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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;
}
Expand Down
8 changes: 7 additions & 1 deletion lib/Doctrine/ORM/Internal/Hydration/ObjectHydrator.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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));
Expand Down
5 changes: 5 additions & 0 deletions lib/Doctrine/ORM/Internal/Hydration/SimpleObjectHydrator.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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]);
}

Expand Down
11 changes: 11 additions & 0 deletions tests/Doctrine/Tests/Models/GH10288/GH10288People.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
<?php

declare(strict_types=1);

namespace Doctrine\Tests\Models\GH10288;

enum GH10288People: string
{
case BOSS = 'boss';
case EMPLOYEE = 'employee';
}
152 changes: 152 additions & 0 deletions tests/Doctrine/Tests/ORM/Functional/Ticket/GH10288Test.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,152 @@
<?php

declare(strict_types=1);

namespace Doctrine\Tests\ORM\Functional\Ticket;

use Doctrine\DBAL\Platforms\AbstractPlatform;
use Doctrine\DBAL\Types\StringType;
use Doctrine\DBAL\Types\Type;
use Doctrine\ORM\AbstractQuery;
use Doctrine\ORM\Mapping\Column;
use Doctrine\ORM\Mapping\DiscriminatorColumn;
use Doctrine\ORM\Mapping\DiscriminatorMap;
use Doctrine\ORM\Mapping\Entity;
use Doctrine\ORM\Mapping\GeneratedValue;
use Doctrine\ORM\Mapping\Id;
use Doctrine\ORM\Mapping\InheritanceType;
use Doctrine\Tests\Models\GH10288\GH10288People;
use Doctrine\Tests\OrmFunctionalTestCase;

/**
* @requires PHP 8.1
*/
class GH10288Test extends OrmFunctionalTestCase
{
protected function setUp(): void
{
parent::setUp();

Type::addType(GH10288PeopleType::NAME, GH10288PeopleType::class);

$this->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
{
}

0 comments on commit 0ef8084

Please sign in to comment.