Skip to content

Commit

Permalink
Merge pull request #9304 from beberlei/EnumSupport
Browse files Browse the repository at this point in the history
Add support for PHP 8.1 enums.
  • Loading branch information
greg0ire authored Jan 8, 2022
2 parents 6f54011 + 2d475c9 commit 4117ca3
Show file tree
Hide file tree
Showing 20 changed files with 441 additions and 2 deletions.
5 changes: 5 additions & 0 deletions docs/en/reference/basic-mapping.rst
Original file line number Diff line number Diff line change
Expand Up @@ -199,6 +199,8 @@ list:
unique key.
- ``nullable``: (optional, default FALSE) Whether the database
column is nullable.
- ``enumType``: (optional, requires PHP 8.1 and ORM 2.11) The PHP enum type
name to convert the database value into.
- ``precision``: (optional, default 0) The precision for a decimal
(exact numeric) column (applies only for decimal column),
which is the maximum number of digits that are stored for the values.
Expand Down Expand Up @@ -233,6 +235,9 @@ Additionally, Doctrine will map PHP types to ``type`` attribute as follows:
- ``int``: ``integer``
- ``string`` or any other type: ``string``

As of version 2.11 Doctrine can also automatically map typed properties using a
PHP 8.1 enum to set the right ``type`` and ``enumType``.

.. _reference-mapping-types:

Doctrine Mapping Types
Expand Down
1 change: 1 addition & 0 deletions doctrine-mapping.xsd
Original file line number Diff line number Diff line change
Expand Up @@ -299,6 +299,7 @@
<xs:attribute name="length" type="xs:NMTOKEN" />
<xs:attribute name="unique" type="xs:boolean" default="false" />
<xs:attribute name="nullable" type="xs:boolean" default="false" />
<xs:attribute name="enum-type" type="xs:string" />
<xs:attribute name="version" type="xs:boolean" />
<xs:attribute name="column-definition" type="xs:string" />
<xs:attribute name="precision" type="xs:integer" use="optional" />
Expand Down
30 changes: 30 additions & 0 deletions lib/Doctrine/ORM/Mapping/ClassMetadataInfo.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

namespace Doctrine\ORM\Mapping;

use BackedEnum;
use BadMethodCallException;
use DateInterval;
use DateTime;
Expand All @@ -22,6 +23,7 @@
use InvalidArgumentException;
use LogicException;
use ReflectionClass;
use ReflectionEnum;
use ReflectionNamedType;
use ReflectionProperty;
use RuntimeException;
Expand All @@ -37,6 +39,7 @@
use function assert;
use function class_exists;
use function count;
use function enum_exists;
use function explode;
use function gettype;
use function in_array;
Expand Down Expand Up @@ -77,6 +80,7 @@
* length?: int,
* id?: bool,
* nullable?: bool,
* enumType?: class-string<BackedEnum>,
* columnDefinition?: string,
* precision?: int,
* scale?: int,
Expand Down Expand Up @@ -1025,6 +1029,13 @@ public function wakeupReflection($reflService)
$this->reflFields[$field] = isset($mapping['declared'])
? $reflService->getAccessibleProperty($mapping['declared'], $field)
: $reflService->getAccessibleProperty($this->name, $field);

if (isset($mapping['enumType']) && $this->reflFields[$field] !== null) {
$this->reflFields[$field] = new ReflectionEnumProperty(
$this->reflFields[$field],
$mapping['enumType']
);
}
}

foreach ($this->associationMappings as $field => $mapping) {
Expand Down Expand Up @@ -1468,6 +1479,15 @@ private function validateAndCompleteTypedFieldMapping(array $mapping): array
! isset($mapping['type'])
&& ($type instanceof ReflectionNamedType)
) {
if (PHP_VERSION_ID >= 80100 && ! $type->isBuiltin() && enum_exists($type->getName(), false)) {
$mapping['enumType'] = $type->getName();

$reflection = new ReflectionEnum($type->getName());
$type = $reflection->getBackingType();

assert($type instanceof ReflectionNamedType);
}

switch ($type->getName()) {
case DateInterval::class:
$mapping['type'] = Types::DATEINTERVAL;
Expand Down Expand Up @@ -1589,6 +1609,16 @@ protected function validateAndCompleteFieldMapping(array $mapping): array
$mapping['requireSQLConversion'] = true;
}

if (isset($mapping['enumType'])) {
if (PHP_VERSION_ID < 80100) {
throw MappingException::enumsRequirePhp81($this->name, $mapping['fieldName']);
}

if (! enum_exists($mapping['enumType'])) {
throw MappingException::nonEnumTypeMapped($this->name, $mapping['fieldName'], $mapping['enumType']);
}
}

return $mapping;
}

Expand Down
8 changes: 7 additions & 1 deletion lib/Doctrine/ORM/Mapping/Column.php
Original file line number Diff line number Diff line change
Expand Up @@ -44,14 +44,18 @@ final class Column implements Annotation
/** @var bool */
public $nullable = false;

/** @var class-string<\BackedEnum>|null */
public $enumType = null;

/** @var array<string,mixed> */
public $options = [];

/** @var string|null */
public $columnDefinition;

/**
* @param array<string,mixed> $options
* @param class-string<\BackedEnum>|null $enumType
* @param array<string,mixed> $options
*/
public function __construct(
?string $name = null,
Expand All @@ -61,6 +65,7 @@ public function __construct(
?int $scale = null,
bool $unique = false,
bool $nullable = false,
?string $enumType = null,
array $options = [],
?string $columnDefinition = null
) {
Expand All @@ -71,6 +76,7 @@ public function __construct(
$this->scale = $scale;
$this->unique = $unique;
$this->nullable = $nullable;
$this->enumType = $enumType;
$this->options = $options;
$this->columnDefinition = $columnDefinition;
}
Expand Down
5 changes: 5 additions & 0 deletions lib/Doctrine/ORM/Mapping/Driver/AnnotationDriver.php
Original file line number Diff line number Diff line change
Expand Up @@ -718,6 +718,7 @@ private function joinColumnToArray(Mapping\JoinColumn $joinColumn): array
* unique: bool,
* nullable: bool,
* precision: int,
* enumType?: class-string,
* options?: mixed[],
* columnName?: string,
* columnDefinition?: string
Expand Down Expand Up @@ -747,6 +748,10 @@ private function columnToArray(string $fieldName, Mapping\Column $column): array
$mapping['columnDefinition'] = $column->columnDefinition;
}

if ($column->enumType !== null) {
$mapping['enumType'] = $column->enumType;
}

return $mapping;
}

Expand Down
5 changes: 5 additions & 0 deletions lib/Doctrine/ORM/Mapping/Driver/AttributeDriver.php
Original file line number Diff line number Diff line change
Expand Up @@ -614,6 +614,7 @@ private function joinColumnToArray($joinColumn): array
* unique: bool,
* nullable: bool,
* precision: int,
* enumType?: class-string,
* options?: mixed[],
* columnName?: string,
* columnDefinition?: string
Expand Down Expand Up @@ -643,6 +644,10 @@ private function columnToArray(string $fieldName, Mapping\Column $column): array
$mapping['columnDefinition'] = $column->columnDefinition;
}

if ($column->enumType) {
$mapping['enumType'] = $column->enumType;
}

return $mapping;
}
}
5 changes: 5 additions & 0 deletions lib/Doctrine/ORM/Mapping/Driver/XmlDriver.php
Original file line number Diff line number Diff line change
Expand Up @@ -800,6 +800,7 @@ private function joinColumnToArray(SimpleXMLElement $joinColumnElement): array
* scale?: int,
* unique?: bool,
* nullable?: bool,
* enumType?: string,
* version?: bool,
* columnDefinition?: string,
* options?: array
Expand Down Expand Up @@ -847,6 +848,10 @@ private function columnToArray(SimpleXMLElement $fieldMapping): array
$mapping['columnDefinition'] = (string) $fieldMapping['column-definition'];
}

if (isset($fieldMapping['enum-type'])) {
$mapping['enumType'] = (string) $fieldMapping['enum-type'];
}

if (isset($fieldMapping->options)) {
$mapping['options'] = $this->parseOptions($fieldMapping->options->children());
}
Expand Down
6 changes: 6 additions & 0 deletions lib/Doctrine/ORM/Mapping/Driver/YamlDriver.php
Original file line number Diff line number Diff line change
Expand Up @@ -786,6 +786,7 @@ private function joinColumnToArray(array $joinColumnElement): array
* unique?: mixed,
* options?: mixed,
* nullable?: mixed,
* enumType?: class-string,
* version?: mixed,
* columnDefinition?: mixed
* }|null $column
Expand All @@ -801,6 +802,7 @@ private function joinColumnToArray(array $joinColumnElement): array
* unique?: bool,
* options?: mixed,
* nullable?: mixed,
* enumType?: class-string,
* version?: mixed,
* columnDefinition?: mixed
* }
Expand Down Expand Up @@ -856,6 +858,10 @@ private function columnToArray(string $fieldName, ?array $column): array
$mapping['columnDefinition'] = $column['columnDefinition'];
}

if (isset($column['enumType'])) {
$mapping['enumType'] = $column['enumType'];
}

return $mapping;
}

Expand Down
42 changes: 42 additions & 0 deletions lib/Doctrine/ORM/Mapping/MappingException.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,10 @@

namespace Doctrine\ORM\Mapping;

use BackedEnum;
use Doctrine\ORM\Exception\ORMException;
use ReflectionException;
use ValueError;

use function array_keys;
use function array_map;
Expand Down Expand Up @@ -953,4 +955,44 @@ public static function invalidOverrideType(string $expectdType, $givenValue): se
get_debug_type($givenValue)
));
}

public static function enumsRequirePhp81(string $className, string $fieldName): self
{
return new self(sprintf('Enum types require PHP 8.1 in %s::$%s', $className, $fieldName));
}

public static function nonEnumTypeMapped(string $className, string $fieldName, string $enumType): self
{
return new self(sprintf(
'Attempting to map non-enum type %s as enum in entity %s::$%s',
$enumType,
$className,
$fieldName
));
}

/**
* @param class-string $className
* @param class-string<BackedEnum> $enumType
*/
public static function invalidEnumValue(
string $className,
string $fieldName,
string $value,
string $enumType,
ValueError $previous
): self {
return new self(sprintf(
<<<'EXCEPTION'
Context: Trying to hydrate enum property "%s::$%s"
Problem: Case "%s" is not listed in enum "%s"
Solution: Either add the case to the enum type or migrate the database column to use another case of the enum
EXCEPTION
,
$className,
$fieldName,
$value,
$enumType
), 0, $previous);
}
}
82 changes: 82 additions & 0 deletions lib/Doctrine/ORM/Mapping/ReflectionEnumProperty.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
<?php

declare(strict_types=1);

namespace Doctrine\ORM\Mapping;

use BackedEnum;
use ReflectionProperty;
use ReturnTypeWillChange;
use ValueError;

use function assert;
use function get_class;
use function is_int;
use function is_string;

class ReflectionEnumProperty extends ReflectionProperty
{
/** @var ReflectionProperty */
private $originalReflectionProperty;

/** @var class-string<BackedEnum> */
private $enumType;

/**
* @param class-string<BackedEnum> $enumType
*/
public function __construct(ReflectionProperty $originalReflectionProperty, string $enumType)
{
$this->originalReflectionProperty = $originalReflectionProperty;
$this->enumType = $enumType;
}

/**
* {@inheritDoc}
*
* @param object|null $object
*
* @return int|string|null
*/
#[ReturnTypeWillChange]
public function getValue($object = null)
{
if ($object === null) {
return null;
}

$enum = $this->originalReflectionProperty->getValue($object);

if ($enum === null) {
return null;
}

return $enum->value;
}

/**
* @param object $object
* @param mixed $value
*/
public function setValue($object, $value = null): void
{
if ($value !== null) {
$enumType = $this->enumType;
try {
$value = $enumType::from($value);
} catch (ValueError $e) {
assert(is_string($value) || is_int($value));

throw MappingException::invalidEnumValue(
get_class($object),
$this->originalReflectionProperty->getName(),
(string) $value,
$enumType,
$e
);
}
}

$this->originalReflectionProperty->setValue($object, $value);
}
}
9 changes: 9 additions & 0 deletions phpcs.xml.dist
Original file line number Diff line number Diff line change
Expand Up @@ -265,4 +265,13 @@
<!-- https://github.com/doctrine/orm/issues/8537 -->
<exclude-pattern>lib/Doctrine/ORM/QueryBuilder.php</exclude-pattern>
</rule>

<rule ref="Generic.WhiteSpace.ScopeIndent.Incorrect">
<!-- see https://github.com/squizlabs/PHP_CodeSniffer/issues/3474 -->
<exclude-pattern>tests/Doctrine/Tests/Models/Enums/Suit.php</exclude-pattern>
</rule>
<rule ref="Generic.WhiteSpace.ScopeIndent.IncorrectExact">
<!-- see https://github.com/squizlabs/PHP_CodeSniffer/issues/3474 -->
<exclude-pattern>tests/Doctrine/Tests/Models/Enums/Suit.php</exclude-pattern>
</rule>
</ruleset>
2 changes: 1 addition & 1 deletion phpstan-baseline.neon
Original file line number Diff line number Diff line change
Expand Up @@ -1746,7 +1746,7 @@ parameters:
path: lib/Doctrine/ORM/Tools/Export/Driver/XmlExporter.php

-
message: "#^Offset 'version' on array\\{type\\: string, fieldName\\: string, columnName\\?\\: string, length\\?\\: int, id\\?\\: bool, nullable\\?\\: bool, columnDefinition\\?\\: string, precision\\?\\: int, \\.\\.\\.\\} in isset\\(\\) does not exist\\.$#"
message: "#^Offset 'version' on array\\{type\\: string, fieldName\\: string, columnName\\?\\: string, length\\?\\: int, id\\?\\: bool, nullable\\?\\: bool, enumType\\?\\: class\\-string\\<BackedEnum\\>, columnDefinition\\?\\: string, \\.\\.\\.\\} in isset\\(\\) does not exist\\.$#"
count: 1
path: lib/Doctrine/ORM/Tools/Export/Driver/XmlExporter.php

Expand Down
Loading

0 comments on commit 4117ca3

Please sign in to comment.