Skip to content

Commit af1350b

Browse files
committed
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)
1 parent 70b5f6e commit af1350b

File tree

3 files changed

+248
-1
lines changed

3 files changed

+248
-1
lines changed

lib/Doctrine/ORM/Internal/Hydration/ObjectHydrator.php

+7-1
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44

55
namespace Doctrine\ORM\Internal\Hydration;
66

7+
use BackedEnum;
78
use Doctrine\Common\Collections\ArrayCollection;
89
use Doctrine\ORM\Mapping\ClassMetadata;
910
use Doctrine\ORM\PersistentCollection;
@@ -246,7 +247,12 @@ private function getEntity(array $data, string $dqlAlias)
246247
}
247248

248249
$discrMap = $this->_metadataCache[$className]->discriminatorMap;
249-
$discriminatorValue = (string) $data[$discrColumn];
250+
$discriminatorValue = $data[$discrColumn];
251+
if ($discriminatorValue instanceof BackedEnum) {
252+
$discriminatorValue = $discriminatorValue->value;
253+
}
254+
255+
$discriminatorValue = (string) $discriminatorValue;
250256

251257
if (! isset($discrMap[$discriminatorValue])) {
252258
throw HydrationException::invalidDiscriminatorValue($discriminatorValue, array_keys($discrMap));
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Doctrine\Tests\Models\GH10288;
6+
7+
enum GH10288People: string
8+
{
9+
case BOSS = 'boss';
10+
case EMPLOYEE = 'employee';
11+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,230 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Doctrine\Tests\ORM\Functional\Ticket;
6+
7+
use Doctrine\DBAL\Platforms\AbstractPlatform;
8+
use Doctrine\DBAL\Types\StringType;
9+
use Doctrine\DBAL\Types\Type;
10+
use Doctrine\ORM\AbstractQuery;
11+
use Doctrine\ORM\Mapping\Column;
12+
use Doctrine\ORM\Mapping\DiscriminatorColumn;
13+
use Doctrine\ORM\Mapping\DiscriminatorMap;
14+
use Doctrine\ORM\Mapping\Entity;
15+
use Doctrine\ORM\Mapping\GeneratedValue;
16+
use Doctrine\ORM\Mapping\Id;
17+
use Doctrine\ORM\Mapping\InheritanceType;
18+
use Doctrine\Tests\Models\GH10288\GH10288People;
19+
use Doctrine\Tests\OrmFunctionalTestCase;
20+
21+
/**
22+
* @requires PHP 8.1
23+
*/
24+
class GH10288Test extends OrmFunctionalTestCase
25+
{
26+
protected function setUp(): void
27+
{
28+
parent::setUp();
29+
30+
Type::addType(GH10288PeopleType::NAME, GH10288PeopleType::class);
31+
32+
$this->createSchemaForModels(
33+
GH10288PersonJoined::class,
34+
GH10288BossJoined::class,
35+
GH10288EmployeeJoined::class,
36+
GH10288PersonSingleTable::class,
37+
GH10288BossSingleTable::class,
38+
GH10288EmployeeSingleTable::class
39+
);
40+
}
41+
42+
/**
43+
* The intent of this test is to ensure that the ORM is capable
44+
* of using enums as discriminators (which makes things a bit
45+
* more dynamic as you can see on the mapping of `GH10288Person`)
46+
* Both JOINED and SINGLE_TABLE inheritances are used
47+
*
48+
* @group GH-10288
49+
*/
50+
public function testEnumDiscriminatorsShouldBeConvertedToString(): void
51+
{
52+
$boss = new GH10288BossJoined('John');
53+
$boss->bossId = 1;
54+
$employee = new GH10288EmployeeJoined('Bob');
55+
56+
$boss2 = new GH10288BossSingleTable('John');
57+
$boss2->bossId = 1;
58+
$employee2 = new GH10288EmployeeSingleTable('Bob');
59+
60+
$this->_em->persist($boss);
61+
$this->_em->persist($boss2);
62+
$this->_em->persist($employee);
63+
$this->_em->persist($employee2);
64+
$this->_em->flush();
65+
$this->_em->clear();
66+
67+
// Using DQL here to make sure that we'll use ObjectHydrator instead of SimpleObjectHydrator
68+
$query = $this->_em->createQueryBuilder()
69+
->select('person')
70+
->from(GH10288PersonJoined::class, 'person')
71+
->where('person.name = :name')
72+
->setMaxResults(1)
73+
->getQuery();
74+
75+
$query->setParameter('name', 'John');
76+
self::assertEquals($boss, $query->getOneOrNullResult(AbstractQuery::HYDRATE_OBJECT));
77+
self::assertEquals(
78+
GH10288People::BOSS,
79+
$query->getOneOrNullResult(AbstractQuery::HYDRATE_ARRAY)['discr']
80+
);
81+
82+
$bossId = $boss->id;
83+
84+
// Using DQL here to make sure that we'll use ObjectHydrator instead of SimpleObjectHydrator
85+
$query2 = $this->_em->createQueryBuilder()
86+
->select('person')
87+
->from(GH10288PersonSingleTable::class, 'person')
88+
->where('person.name = :name')
89+
->setMaxResults(1)
90+
->getQuery();
91+
92+
$query2->setParameter('name', 'Bob');
93+
self::assertEquals($employee2, $query2->getOneOrNullResult(AbstractQuery::HYDRATE_OBJECT));
94+
self::assertEquals(
95+
GH10288People::EMPLOYEE,
96+
$query2->getOneOrNullResult(AbstractQuery::HYDRATE_ARRAY)['discr']
97+
);
98+
99+
$this->_em->clear();
100+
101+
// test SimpleObjectHydrator
102+
$bossFetched = $this->_em->find(GH10288PersonJoined::class, $bossId);
103+
self::assertEquals($boss, $bossFetched);
104+
}
105+
}
106+
107+
class GH10288PeopleType extends StringType
108+
{
109+
public const NAME = 'GH10288people';
110+
111+
/**
112+
* {@inheritdoc}
113+
*/
114+
public function convertToDatabaseValue($value, AbstractPlatform $platform)
115+
{
116+
if (! $value instanceof GH10288People) {
117+
$value = GH10288People::from($value);
118+
}
119+
120+
return $value->value;
121+
}
122+
123+
/**
124+
* {@inheritdoc}
125+
*/
126+
public function convertToPHPValue($value, AbstractPlatform $platform)
127+
{
128+
return GH10288People::from($value);
129+
}
130+
131+
/**
132+
* {@inheritdoc}
133+
*/
134+
public function getName()
135+
{
136+
return self::NAME;
137+
}
138+
}
139+
140+
/**
141+
* @Entity
142+
* @InheritanceType("JOINED")
143+
* @DiscriminatorColumn(name="discr", type="GH10288people")
144+
* @DiscriminatorMap({
145+
* "boss" = GH10288BossJoined::class,
146+
* "employee" = GH10288EmployeeJoined::class
147+
* })
148+
*/
149+
abstract class GH10288PersonJoined
150+
{
151+
/**
152+
* @var int
153+
* @Id
154+
* @Column(type="integer")
155+
* @GeneratedValue(strategy="AUTO")
156+
*/
157+
public $id;
158+
159+
/**
160+
* @var string
161+
* @Column(type="string", length=255)
162+
*/
163+
public $name;
164+
165+
public function __construct(string $name)
166+
{
167+
$this->name = $name;
168+
}
169+
}
170+
171+
/** @Entity */
172+
class GH10288BossJoined extends GH10288PersonJoined
173+
{
174+
/**
175+
* @var int
176+
* @Column(type="integer")
177+
*/
178+
public $bossId;
179+
}
180+
181+
/** @Entity */
182+
class GH10288EmployeeJoined extends GH10288PersonJoined
183+
{
184+
}
185+
186+
/**
187+
* @Entity
188+
* @InheritanceType("SINGLE_TABLE")
189+
* @DiscriminatorColumn(name="discr", type="GH10288people")
190+
* @DiscriminatorMap({
191+
* "boss" = GH10288BossSingleTable::class,
192+
* "employee" = GH10288EmployeeSingleTable::class
193+
* })
194+
*/
195+
abstract class GH10288PersonSingleTable
196+
{
197+
/**
198+
* @var int
199+
* @Id
200+
* @Column(type="integer")
201+
* @GeneratedValue(strategy="AUTO")
202+
*/
203+
public $id;
204+
205+
/**
206+
* @var string
207+
* @Column(type="string", length=255)
208+
*/
209+
public $name;
210+
211+
public function __construct(string $name)
212+
{
213+
$this->name = $name;
214+
}
215+
}
216+
217+
/** @Entity */
218+
class GH10288BossSingleTable extends GH10288PersonSingleTable
219+
{
220+
/**
221+
* @var int
222+
* @Column(type="integer")
223+
*/
224+
public $bossId;
225+
}
226+
227+
/** @Entity */
228+
class GH10288EmployeeSingleTable extends GH10288PersonSingleTable
229+
{
230+
}

0 commit comments

Comments
 (0)