Skip to content

Commit

Permalink
Auto-detect values for EnumType columns (#11666)
Browse files Browse the repository at this point in the history
  • Loading branch information
derrabus authored Oct 12, 2024
1 parent 19d9244 commit 60c2454
Show file tree
Hide file tree
Showing 6 changed files with 111 additions and 31 deletions.
1 change: 1 addition & 0 deletions psalm-baseline.xml
Original file line number Diff line number Diff line change
Expand Up @@ -402,6 +402,7 @@
<file src="src/Mapping/DefaultTypedFieldMapper.php">
<LessSpecificReturnStatement>
<code><![CDATA[$mapping]]></code>
<code><![CDATA[$mapping]]></code>
</LessSpecificReturnStatement>
<MoreSpecificReturnType>
<code><![CDATA[array]]></code>
Expand Down
15 changes: 12 additions & 3 deletions src/Mapping/ClassMetadata.php
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
use BackedEnum;
use BadMethodCallException;
use Doctrine\DBAL\Platforms\AbstractPlatform;
use Doctrine\DBAL\Types\Types;
use Doctrine\Deprecations\Deprecation;
use Doctrine\Instantiator\Instantiator;
use Doctrine\Instantiator\InstantiatorInterface;
Expand All @@ -23,6 +24,7 @@
use ReflectionProperty;
use Stringable;

use function array_column;
use function array_diff;
use function array_intersect;
use function array_key_exists;
Expand All @@ -34,6 +36,7 @@
use function assert;
use function class_exists;
use function count;
use function defined;
use function enum_exists;
use function explode;
use function in_array;
Expand Down Expand Up @@ -1119,9 +1122,7 @@ private function validateAndCompleteTypedFieldMapping(array $mapping): array
{
$field = $this->reflClass->getProperty($mapping['fieldName']);

$mapping = $this->typedFieldMapper->validateAndComplete($mapping, $field);

return $mapping;
return $this->typedFieldMapper->validateAndComplete($mapping, $field);
}

/**
Expand Down Expand Up @@ -1232,6 +1233,14 @@ protected function validateAndCompleteFieldMapping(array $mapping): FieldMapping
if (! empty($mapping->id)) {
$this->containsEnumIdentifier = true;
}

if (
defined('Doctrine\DBAL\Types\Types::ENUM')
&& $mapping->type === Types::ENUM
&& ! isset($mapping->options['values'])
) {
$mapping->options['values'] = array_column($mapping->enumType::cases(), 'value');
}
}

return $mapping;
Expand Down
49 changes: 30 additions & 19 deletions src/Mapping/DefaultTypedFieldMapper.php
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@

use function array_merge;
use function assert;
use function defined;
use function enum_exists;
use function is_a;

Expand Down Expand Up @@ -49,30 +50,40 @@ public function validateAndComplete(array $mapping, ReflectionProperty $field):
{
$type = $field->getType();

if (! $type instanceof ReflectionNamedType) {
return $mapping;
}

if (
! isset($mapping['type'])
&& ($type instanceof ReflectionNamedType)
! $type->isBuiltin()
&& enum_exists($type->getName())
&& (! isset($mapping['type']) || (
defined('Doctrine\DBAL\Types\Types::ENUM')
&& $mapping['type'] === Types::ENUM
))
) {
if (! $type->isBuiltin() && enum_exists($type->getName())) {
$reflection = new ReflectionEnum($type->getName());
if (! $reflection->isBacked()) {
throw MappingException::backedEnumTypeRequired(
$field->class,
$mapping['fieldName'],
$type->getName(),
);
}
$reflection = new ReflectionEnum($type->getName());
if (! $reflection->isBacked()) {
throw MappingException::backedEnumTypeRequired(
$field->class,
$mapping['fieldName'],
$type->getName(),
);
}

assert(is_a($type->getName(), BackedEnum::class, true));
$mapping['enumType'] = $type->getName();
$type = $reflection->getBackingType();
assert(is_a($type->getName(), BackedEnum::class, true));
$mapping['enumType'] = $type->getName();
$type = $reflection->getBackingType();

assert($type instanceof ReflectionNamedType);
}
assert($type instanceof ReflectionNamedType);
}

if (isset($this->typedFieldMappings[$type->getName()])) {
$mapping['type'] = $this->typedFieldMappings[$type->getName()];
}
if (isset($mapping['type'])) {
return $mapping;
}

if (isset($this->typedFieldMappings[$type->getName()])) {
$mapping['type'] = $this->typedFieldMappings[$type->getName()];
}

return $mapping;
Expand Down
25 changes: 25 additions & 0 deletions tests/Tests/Models/Enums/CardNativeEnum.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
<?php

declare(strict_types=1);

namespace Doctrine\Tests\Models\Enums;

use Doctrine\DBAL\Types\Types;
use Doctrine\ORM\Mapping\Column;
use Doctrine\ORM\Mapping\Entity;
use Doctrine\ORM\Mapping\GeneratedValue;
use Doctrine\ORM\Mapping\Id;

#[Entity]
class CardNativeEnum
{
/** @var int|null */
#[Id]
#[GeneratedValue]
#[Column(type: Types::INTEGER)]
public $id;

/** @var Suit */
#[Column(type: Types::ENUM, enumType: Suit::class, options: ['values' => ['H', 'D', 'C', 'S', 'Z']])]
public $suit;
}
23 changes: 23 additions & 0 deletions tests/Tests/Models/Enums/TypedCardNativeEnum.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
<?php

declare(strict_types=1);

namespace Doctrine\Tests\Models\Enums;

use Doctrine\DBAL\Types\Types;
use Doctrine\ORM\Mapping\Column;
use Doctrine\ORM\Mapping\Entity;
use Doctrine\ORM\Mapping\GeneratedValue;
use Doctrine\ORM\Mapping\Id;

#[Entity]
class TypedCardNativeEnum
{
#[Id]
#[GeneratedValue]
#[Column]
public int $id;

#[Column(type: Types::ENUM)]
public Suit $suit;
}
29 changes: 20 additions & 9 deletions tests/Tests/ORM/Functional/EnumTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

namespace Doctrine\Tests\ORM\Functional;

use Doctrine\DBAL\Types\EnumType;
use Doctrine\ORM\AbstractQuery;
use Doctrine\ORM\Mapping\Column;
use Doctrine\ORM\Mapping\Driver\AttributeDriver;
Expand All @@ -13,17 +14,20 @@
use Doctrine\Tests\Models\DataTransferObjects\DtoWithArrayOfEnums;
use Doctrine\Tests\Models\DataTransferObjects\DtoWithEnum;
use Doctrine\Tests\Models\Enums\Card;
use Doctrine\Tests\Models\Enums\CardNativeEnum;
use Doctrine\Tests\Models\Enums\CardWithDefault;
use Doctrine\Tests\Models\Enums\CardWithNullable;
use Doctrine\Tests\Models\Enums\Product;
use Doctrine\Tests\Models\Enums\Quantity;
use Doctrine\Tests\Models\Enums\Scale;
use Doctrine\Tests\Models\Enums\Suit;
use Doctrine\Tests\Models\Enums\TypedCard;
use Doctrine\Tests\Models\Enums\TypedCardNativeEnum;
use Doctrine\Tests\Models\Enums\Unit;
use Doctrine\Tests\OrmFunctionalTestCase;
use PHPUnit\Framework\Attributes\DataProvider;

use function class_exists;
use function dirname;
use function sprintf;
use function uniqid;
Expand Down Expand Up @@ -55,7 +59,7 @@ public function testEnumMapping(string $cardClass): void
$this->_em->flush();
$this->_em->clear();

$fetchedCard = $this->_em->find(Card::class, $card->id);
$fetchedCard = $this->_em->find($cardClass, $card->id);

$this->assertInstanceOf(Suit::class, $fetchedCard->suit);
$this->assertEquals(Suit::Clubs, $fetchedCard->suit);
Expand Down Expand Up @@ -417,6 +421,10 @@ public function testFindByEnum(): void
#[DataProvider('provideCardClasses')]
public function testEnumWithNonMatchingDatabaseValueThrowsException(string $cardClass): void
{
if ($cardClass === TypedCardNativeEnum::class) {
self::markTestSkipped('MySQL won\'t allow us to insert invalid values in this case.');
}

$this->setUpEntitySchema([$cardClass]);

$card = new $cardClass();
Expand All @@ -429,15 +437,15 @@ public function testEnumWithNonMatchingDatabaseValueThrowsException(string $card
$metadata = $this->_em->getClassMetadata($cardClass);
$this->_em->getConnection()->update(
$metadata->table['name'],
[$metadata->fieldMappings['suit']->columnName => 'invalid'],
[$metadata->fieldMappings['suit']->columnName => 'Z'],
[$metadata->fieldMappings['id']->columnName => $card->id],
);

$this->expectException(MappingException::class);
$this->expectExceptionMessage(sprintf(
<<<'EXCEPTION'
Context: Trying to hydrate enum property "%s::$suit"
Problem: Case "invalid" is not listed in enum "Doctrine\Tests\Models\Enums\Suit"
Problem: Case "Z" is not listed in enum "Doctrine\Tests\Models\Enums\Suit"
Solution: Either add the case to the enum type or migrate the database column to use another case of the enum
EXCEPTION
,
Expand All @@ -447,13 +455,16 @@ public function testEnumWithNonMatchingDatabaseValueThrowsException(string $card
$this->_em->find($cardClass, $card->id);
}

/** @return array<string, array{class-string}> */
public static function provideCardClasses(): array
/** @return iterable<string, array{class-string}> */
public static function provideCardClasses(): iterable
{
return [
Card::class => [Card::class],
TypedCard::class => [TypedCard::class],
];
yield Card::class => [Card::class];
yield TypedCard::class => [TypedCard::class];

if (class_exists(EnumType::class)) {
yield CardNativeEnum::class => [CardNativeEnum::class];
yield TypedCardNativeEnum::class => [TypedCardNativeEnum::class];
}
}

public function testItAllowsReadingAttributes(): void
Expand Down

0 comments on commit 60c2454

Please sign in to comment.