Skip to content

Commit 4117ca3

Browse files
authored
Merge pull request #9304 from beberlei/EnumSupport
Add support for PHP 8.1 enums.
2 parents 6f54011 + 2d475c9 commit 4117ca3

20 files changed

+441
-2
lines changed

docs/en/reference/basic-mapping.rst

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -199,6 +199,8 @@ list:
199199
unique key.
200200
- ``nullable``: (optional, default FALSE) Whether the database
201201
column is nullable.
202+
- ``enumType``: (optional, requires PHP 8.1 and ORM 2.11) The PHP enum type
203+
name to convert the database value into.
202204
- ``precision``: (optional, default 0) The precision for a decimal
203205
(exact numeric) column (applies only for decimal column),
204206
which is the maximum number of digits that are stored for the values.
@@ -233,6 +235,9 @@ Additionally, Doctrine will map PHP types to ``type`` attribute as follows:
233235
- ``int``: ``integer``
234236
- ``string`` or any other type: ``string``
235237

238+
As of version 2.11 Doctrine can also automatically map typed properties using a
239+
PHP 8.1 enum to set the right ``type`` and ``enumType``.
240+
236241
.. _reference-mapping-types:
237242

238243
Doctrine Mapping Types

doctrine-mapping.xsd

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -299,6 +299,7 @@
299299
<xs:attribute name="length" type="xs:NMTOKEN" />
300300
<xs:attribute name="unique" type="xs:boolean" default="false" />
301301
<xs:attribute name="nullable" type="xs:boolean" default="false" />
302+
<xs:attribute name="enum-type" type="xs:string" />
302303
<xs:attribute name="version" type="xs:boolean" />
303304
<xs:attribute name="column-definition" type="xs:string" />
304305
<xs:attribute name="precision" type="xs:integer" use="optional" />

lib/Doctrine/ORM/Mapping/ClassMetadataInfo.php

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44

55
namespace Doctrine\ORM\Mapping;
66

7+
use BackedEnum;
78
use BadMethodCallException;
89
use DateInterval;
910
use DateTime;
@@ -22,6 +23,7 @@
2223
use InvalidArgumentException;
2324
use LogicException;
2425
use ReflectionClass;
26+
use ReflectionEnum;
2527
use ReflectionNamedType;
2628
use ReflectionProperty;
2729
use RuntimeException;
@@ -37,6 +39,7 @@
3739
use function assert;
3840
use function class_exists;
3941
use function count;
42+
use function enum_exists;
4043
use function explode;
4144
use function gettype;
4245
use function in_array;
@@ -77,6 +80,7 @@
7780
* length?: int,
7881
* id?: bool,
7982
* nullable?: bool,
83+
* enumType?: class-string<BackedEnum>,
8084
* columnDefinition?: string,
8185
* precision?: int,
8286
* scale?: int,
@@ -1025,6 +1029,13 @@ public function wakeupReflection($reflService)
10251029
$this->reflFields[$field] = isset($mapping['declared'])
10261030
? $reflService->getAccessibleProperty($mapping['declared'], $field)
10271031
: $reflService->getAccessibleProperty($this->name, $field);
1032+
1033+
if (isset($mapping['enumType']) && $this->reflFields[$field] !== null) {
1034+
$this->reflFields[$field] = new ReflectionEnumProperty(
1035+
$this->reflFields[$field],
1036+
$mapping['enumType']
1037+
);
1038+
}
10281039
}
10291040

10301041
foreach ($this->associationMappings as $field => $mapping) {
@@ -1468,6 +1479,15 @@ private function validateAndCompleteTypedFieldMapping(array $mapping): array
14681479
! isset($mapping['type'])
14691480
&& ($type instanceof ReflectionNamedType)
14701481
) {
1482+
if (PHP_VERSION_ID >= 80100 && ! $type->isBuiltin() && enum_exists($type->getName(), false)) {
1483+
$mapping['enumType'] = $type->getName();
1484+
1485+
$reflection = new ReflectionEnum($type->getName());
1486+
$type = $reflection->getBackingType();
1487+
1488+
assert($type instanceof ReflectionNamedType);
1489+
}
1490+
14711491
switch ($type->getName()) {
14721492
case DateInterval::class:
14731493
$mapping['type'] = Types::DATEINTERVAL;
@@ -1589,6 +1609,16 @@ protected function validateAndCompleteFieldMapping(array $mapping): array
15891609
$mapping['requireSQLConversion'] = true;
15901610
}
15911611

1612+
if (isset($mapping['enumType'])) {
1613+
if (PHP_VERSION_ID < 80100) {
1614+
throw MappingException::enumsRequirePhp81($this->name, $mapping['fieldName']);
1615+
}
1616+
1617+
if (! enum_exists($mapping['enumType'])) {
1618+
throw MappingException::nonEnumTypeMapped($this->name, $mapping['fieldName'], $mapping['enumType']);
1619+
}
1620+
}
1621+
15921622
return $mapping;
15931623
}
15941624

lib/Doctrine/ORM/Mapping/Column.php

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -44,14 +44,18 @@ final class Column implements Annotation
4444
/** @var bool */
4545
public $nullable = false;
4646

47+
/** @var class-string<\BackedEnum>|null */
48+
public $enumType = null;
49+
4750
/** @var array<string,mixed> */
4851
public $options = [];
4952

5053
/** @var string|null */
5154
public $columnDefinition;
5255

5356
/**
54-
* @param array<string,mixed> $options
57+
* @param class-string<\BackedEnum>|null $enumType
58+
* @param array<string,mixed> $options
5559
*/
5660
public function __construct(
5761
?string $name = null,
@@ -61,6 +65,7 @@ public function __construct(
6165
?int $scale = null,
6266
bool $unique = false,
6367
bool $nullable = false,
68+
?string $enumType = null,
6469
array $options = [],
6570
?string $columnDefinition = null
6671
) {
@@ -71,6 +76,7 @@ public function __construct(
7176
$this->scale = $scale;
7277
$this->unique = $unique;
7378
$this->nullable = $nullable;
79+
$this->enumType = $enumType;
7480
$this->options = $options;
7581
$this->columnDefinition = $columnDefinition;
7682
}

lib/Doctrine/ORM/Mapping/Driver/AnnotationDriver.php

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -718,6 +718,7 @@ private function joinColumnToArray(Mapping\JoinColumn $joinColumn): array
718718
* unique: bool,
719719
* nullable: bool,
720720
* precision: int,
721+
* enumType?: class-string,
721722
* options?: mixed[],
722723
* columnName?: string,
723724
* columnDefinition?: string
@@ -747,6 +748,10 @@ private function columnToArray(string $fieldName, Mapping\Column $column): array
747748
$mapping['columnDefinition'] = $column->columnDefinition;
748749
}
749750

751+
if ($column->enumType !== null) {
752+
$mapping['enumType'] = $column->enumType;
753+
}
754+
750755
return $mapping;
751756
}
752757

lib/Doctrine/ORM/Mapping/Driver/AttributeDriver.php

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -614,6 +614,7 @@ private function joinColumnToArray($joinColumn): array
614614
* unique: bool,
615615
* nullable: bool,
616616
* precision: int,
617+
* enumType?: class-string,
617618
* options?: mixed[],
618619
* columnName?: string,
619620
* columnDefinition?: string
@@ -643,6 +644,10 @@ private function columnToArray(string $fieldName, Mapping\Column $column): array
643644
$mapping['columnDefinition'] = $column->columnDefinition;
644645
}
645646

647+
if ($column->enumType) {
648+
$mapping['enumType'] = $column->enumType;
649+
}
650+
646651
return $mapping;
647652
}
648653
}

lib/Doctrine/ORM/Mapping/Driver/XmlDriver.php

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -800,6 +800,7 @@ private function joinColumnToArray(SimpleXMLElement $joinColumnElement): array
800800
* scale?: int,
801801
* unique?: bool,
802802
* nullable?: bool,
803+
* enumType?: string,
803804
* version?: bool,
804805
* columnDefinition?: string,
805806
* options?: array
@@ -847,6 +848,10 @@ private function columnToArray(SimpleXMLElement $fieldMapping): array
847848
$mapping['columnDefinition'] = (string) $fieldMapping['column-definition'];
848849
}
849850

851+
if (isset($fieldMapping['enum-type'])) {
852+
$mapping['enumType'] = (string) $fieldMapping['enum-type'];
853+
}
854+
850855
if (isset($fieldMapping->options)) {
851856
$mapping['options'] = $this->parseOptions($fieldMapping->options->children());
852857
}

lib/Doctrine/ORM/Mapping/Driver/YamlDriver.php

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -786,6 +786,7 @@ private function joinColumnToArray(array $joinColumnElement): array
786786
* unique?: mixed,
787787
* options?: mixed,
788788
* nullable?: mixed,
789+
* enumType?: class-string,
789790
* version?: mixed,
790791
* columnDefinition?: mixed
791792
* }|null $column
@@ -801,6 +802,7 @@ private function joinColumnToArray(array $joinColumnElement): array
801802
* unique?: bool,
802803
* options?: mixed,
803804
* nullable?: mixed,
805+
* enumType?: class-string,
804806
* version?: mixed,
805807
* columnDefinition?: mixed
806808
* }
@@ -856,6 +858,10 @@ private function columnToArray(string $fieldName, ?array $column): array
856858
$mapping['columnDefinition'] = $column['columnDefinition'];
857859
}
858860

861+
if (isset($column['enumType'])) {
862+
$mapping['enumType'] = $column['enumType'];
863+
}
864+
859865
return $mapping;
860866
}
861867

lib/Doctrine/ORM/Mapping/MappingException.php

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,10 @@
44

55
namespace Doctrine\ORM\Mapping;
66

7+
use BackedEnum;
78
use Doctrine\ORM\Exception\ORMException;
89
use ReflectionException;
10+
use ValueError;
911

1012
use function array_keys;
1113
use function array_map;
@@ -953,4 +955,44 @@ public static function invalidOverrideType(string $expectdType, $givenValue): se
953955
get_debug_type($givenValue)
954956
));
955957
}
958+
959+
public static function enumsRequirePhp81(string $className, string $fieldName): self
960+
{
961+
return new self(sprintf('Enum types require PHP 8.1 in %s::$%s', $className, $fieldName));
962+
}
963+
964+
public static function nonEnumTypeMapped(string $className, string $fieldName, string $enumType): self
965+
{
966+
return new self(sprintf(
967+
'Attempting to map non-enum type %s as enum in entity %s::$%s',
968+
$enumType,
969+
$className,
970+
$fieldName
971+
));
972+
}
973+
974+
/**
975+
* @param class-string $className
976+
* @param class-string<BackedEnum> $enumType
977+
*/
978+
public static function invalidEnumValue(
979+
string $className,
980+
string $fieldName,
981+
string $value,
982+
string $enumType,
983+
ValueError $previous
984+
): self {
985+
return new self(sprintf(
986+
<<<'EXCEPTION'
987+
Context: Trying to hydrate enum property "%s::$%s"
988+
Problem: Case "%s" is not listed in enum "%s"
989+
Solution: Either add the case to the enum type or migrate the database column to use another case of the enum
990+
EXCEPTION
991+
,
992+
$className,
993+
$fieldName,
994+
$value,
995+
$enumType
996+
), 0, $previous);
997+
}
956998
}
Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Doctrine\ORM\Mapping;
6+
7+
use BackedEnum;
8+
use ReflectionProperty;
9+
use ReturnTypeWillChange;
10+
use ValueError;
11+
12+
use function assert;
13+
use function get_class;
14+
use function is_int;
15+
use function is_string;
16+
17+
class ReflectionEnumProperty extends ReflectionProperty
18+
{
19+
/** @var ReflectionProperty */
20+
private $originalReflectionProperty;
21+
22+
/** @var class-string<BackedEnum> */
23+
private $enumType;
24+
25+
/**
26+
* @param class-string<BackedEnum> $enumType
27+
*/
28+
public function __construct(ReflectionProperty $originalReflectionProperty, string $enumType)
29+
{
30+
$this->originalReflectionProperty = $originalReflectionProperty;
31+
$this->enumType = $enumType;
32+
}
33+
34+
/**
35+
* {@inheritDoc}
36+
*
37+
* @param object|null $object
38+
*
39+
* @return int|string|null
40+
*/
41+
#[ReturnTypeWillChange]
42+
public function getValue($object = null)
43+
{
44+
if ($object === null) {
45+
return null;
46+
}
47+
48+
$enum = $this->originalReflectionProperty->getValue($object);
49+
50+
if ($enum === null) {
51+
return null;
52+
}
53+
54+
return $enum->value;
55+
}
56+
57+
/**
58+
* @param object $object
59+
* @param mixed $value
60+
*/
61+
public function setValue($object, $value = null): void
62+
{
63+
if ($value !== null) {
64+
$enumType = $this->enumType;
65+
try {
66+
$value = $enumType::from($value);
67+
} catch (ValueError $e) {
68+
assert(is_string($value) || is_int($value));
69+
70+
throw MappingException::invalidEnumValue(
71+
get_class($object),
72+
$this->originalReflectionProperty->getName(),
73+
(string) $value,
74+
$enumType,
75+
$e
76+
);
77+
}
78+
}
79+
80+
$this->originalReflectionProperty->setValue($object, $value);
81+
}
82+
}

0 commit comments

Comments
 (0)