Skip to content

Commit 60c2454

Browse files
authored
Auto-detect values for EnumType columns (#11666)
1 parent 19d9244 commit 60c2454

File tree

6 files changed

+111
-31
lines changed

6 files changed

+111
-31
lines changed

psalm-baseline.xml

+1
Original file line numberDiff line numberDiff line change
@@ -402,6 +402,7 @@
402402
<file src="src/Mapping/DefaultTypedFieldMapper.php">
403403
<LessSpecificReturnStatement>
404404
<code><![CDATA[$mapping]]></code>
405+
<code><![CDATA[$mapping]]></code>
405406
</LessSpecificReturnStatement>
406407
<MoreSpecificReturnType>
407408
<code><![CDATA[array]]></code>

src/Mapping/ClassMetadata.php

+12-3
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
use BackedEnum;
88
use BadMethodCallException;
99
use Doctrine\DBAL\Platforms\AbstractPlatform;
10+
use Doctrine\DBAL\Types\Types;
1011
use Doctrine\Deprecations\Deprecation;
1112
use Doctrine\Instantiator\Instantiator;
1213
use Doctrine\Instantiator\InstantiatorInterface;
@@ -23,6 +24,7 @@
2324
use ReflectionProperty;
2425
use Stringable;
2526

27+
use function array_column;
2628
use function array_diff;
2729
use function array_intersect;
2830
use function array_key_exists;
@@ -34,6 +36,7 @@
3436
use function assert;
3537
use function class_exists;
3638
use function count;
39+
use function defined;
3740
use function enum_exists;
3841
use function explode;
3942
use function in_array;
@@ -1119,9 +1122,7 @@ private function validateAndCompleteTypedFieldMapping(array $mapping): array
11191122
{
11201123
$field = $this->reflClass->getProperty($mapping['fieldName']);
11211124

1122-
$mapping = $this->typedFieldMapper->validateAndComplete($mapping, $field);
1123-
1124-
return $mapping;
1125+
return $this->typedFieldMapper->validateAndComplete($mapping, $field);
11251126
}
11261127

11271128
/**
@@ -1232,6 +1233,14 @@ protected function validateAndCompleteFieldMapping(array $mapping): FieldMapping
12321233
if (! empty($mapping->id)) {
12331234
$this->containsEnumIdentifier = true;
12341235
}
1236+
1237+
if (
1238+
defined('Doctrine\DBAL\Types\Types::ENUM')
1239+
&& $mapping->type === Types::ENUM
1240+
&& ! isset($mapping->options['values'])
1241+
) {
1242+
$mapping->options['values'] = array_column($mapping->enumType::cases(), 'value');
1243+
}
12351244
}
12361245

12371246
return $mapping;

src/Mapping/DefaultTypedFieldMapper.php

+30-19
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616

1717
use function array_merge;
1818
use function assert;
19+
use function defined;
1920
use function enum_exists;
2021
use function is_a;
2122

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

53+
if (! $type instanceof ReflectionNamedType) {
54+
return $mapping;
55+
}
56+
5257
if (
53-
! isset($mapping['type'])
54-
&& ($type instanceof ReflectionNamedType)
58+
! $type->isBuiltin()
59+
&& enum_exists($type->getName())
60+
&& (! isset($mapping['type']) || (
61+
defined('Doctrine\DBAL\Types\Types::ENUM')
62+
&& $mapping['type'] === Types::ENUM
63+
))
5564
) {
56-
if (! $type->isBuiltin() && enum_exists($type->getName())) {
57-
$reflection = new ReflectionEnum($type->getName());
58-
if (! $reflection->isBacked()) {
59-
throw MappingException::backedEnumTypeRequired(
60-
$field->class,
61-
$mapping['fieldName'],
62-
$type->getName(),
63-
);
64-
}
65+
$reflection = new ReflectionEnum($type->getName());
66+
if (! $reflection->isBacked()) {
67+
throw MappingException::backedEnumTypeRequired(
68+
$field->class,
69+
$mapping['fieldName'],
70+
$type->getName(),
71+
);
72+
}
6573

66-
assert(is_a($type->getName(), BackedEnum::class, true));
67-
$mapping['enumType'] = $type->getName();
68-
$type = $reflection->getBackingType();
74+
assert(is_a($type->getName(), BackedEnum::class, true));
75+
$mapping['enumType'] = $type->getName();
76+
$type = $reflection->getBackingType();
6977

70-
assert($type instanceof ReflectionNamedType);
71-
}
78+
assert($type instanceof ReflectionNamedType);
79+
}
7280

73-
if (isset($this->typedFieldMappings[$type->getName()])) {
74-
$mapping['type'] = $this->typedFieldMappings[$type->getName()];
75-
}
81+
if (isset($mapping['type'])) {
82+
return $mapping;
83+
}
84+
85+
if (isset($this->typedFieldMappings[$type->getName()])) {
86+
$mapping['type'] = $this->typedFieldMappings[$type->getName()];
7687
}
7788

7889
return $mapping;
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Doctrine\Tests\Models\Enums;
6+
7+
use Doctrine\DBAL\Types\Types;
8+
use Doctrine\ORM\Mapping\Column;
9+
use Doctrine\ORM\Mapping\Entity;
10+
use Doctrine\ORM\Mapping\GeneratedValue;
11+
use Doctrine\ORM\Mapping\Id;
12+
13+
#[Entity]
14+
class CardNativeEnum
15+
{
16+
/** @var int|null */
17+
#[Id]
18+
#[GeneratedValue]
19+
#[Column(type: Types::INTEGER)]
20+
public $id;
21+
22+
/** @var Suit */
23+
#[Column(type: Types::ENUM, enumType: Suit::class, options: ['values' => ['H', 'D', 'C', 'S', 'Z']])]
24+
public $suit;
25+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Doctrine\Tests\Models\Enums;
6+
7+
use Doctrine\DBAL\Types\Types;
8+
use Doctrine\ORM\Mapping\Column;
9+
use Doctrine\ORM\Mapping\Entity;
10+
use Doctrine\ORM\Mapping\GeneratedValue;
11+
use Doctrine\ORM\Mapping\Id;
12+
13+
#[Entity]
14+
class TypedCardNativeEnum
15+
{
16+
#[Id]
17+
#[GeneratedValue]
18+
#[Column]
19+
public int $id;
20+
21+
#[Column(type: Types::ENUM)]
22+
public Suit $suit;
23+
}

tests/Tests/ORM/Functional/EnumTest.php

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

55
namespace Doctrine\Tests\ORM\Functional;
66

7+
use Doctrine\DBAL\Types\EnumType;
78
use Doctrine\ORM\AbstractQuery;
89
use Doctrine\ORM\Mapping\Column;
910
use Doctrine\ORM\Mapping\Driver\AttributeDriver;
@@ -13,17 +14,20 @@
1314
use Doctrine\Tests\Models\DataTransferObjects\DtoWithArrayOfEnums;
1415
use Doctrine\Tests\Models\DataTransferObjects\DtoWithEnum;
1516
use Doctrine\Tests\Models\Enums\Card;
17+
use Doctrine\Tests\Models\Enums\CardNativeEnum;
1618
use Doctrine\Tests\Models\Enums\CardWithDefault;
1719
use Doctrine\Tests\Models\Enums\CardWithNullable;
1820
use Doctrine\Tests\Models\Enums\Product;
1921
use Doctrine\Tests\Models\Enums\Quantity;
2022
use Doctrine\Tests\Models\Enums\Scale;
2123
use Doctrine\Tests\Models\Enums\Suit;
2224
use Doctrine\Tests\Models\Enums\TypedCard;
25+
use Doctrine\Tests\Models\Enums\TypedCardNativeEnum;
2326
use Doctrine\Tests\Models\Enums\Unit;
2427
use Doctrine\Tests\OrmFunctionalTestCase;
2528
use PHPUnit\Framework\Attributes\DataProvider;
2629

30+
use function class_exists;
2731
use function dirname;
2832
use function sprintf;
2933
use function uniqid;
@@ -55,7 +59,7 @@ public function testEnumMapping(string $cardClass): void
5559
$this->_em->flush();
5660
$this->_em->clear();
5761

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

6064
$this->assertInstanceOf(Suit::class, $fetchedCard->suit);
6165
$this->assertEquals(Suit::Clubs, $fetchedCard->suit);
@@ -417,6 +421,10 @@ public function testFindByEnum(): void
417421
#[DataProvider('provideCardClasses')]
418422
public function testEnumWithNonMatchingDatabaseValueThrowsException(string $cardClass): void
419423
{
424+
if ($cardClass === TypedCardNativeEnum::class) {
425+
self::markTestSkipped('MySQL won\'t allow us to insert invalid values in this case.');
426+
}
427+
420428
$this->setUpEntitySchema([$cardClass]);
421429

422430
$card = new $cardClass();
@@ -429,15 +437,15 @@ public function testEnumWithNonMatchingDatabaseValueThrowsException(string $card
429437
$metadata = $this->_em->getClassMetadata($cardClass);
430438
$this->_em->getConnection()->update(
431439
$metadata->table['name'],
432-
[$metadata->fieldMappings['suit']->columnName => 'invalid'],
440+
[$metadata->fieldMappings['suit']->columnName => 'Z'],
433441
[$metadata->fieldMappings['id']->columnName => $card->id],
434442
);
435443

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

450-
/** @return array<string, array{class-string}> */
451-
public static function provideCardClasses(): array
458+
/** @return iterable<string, array{class-string}> */
459+
public static function provideCardClasses(): iterable
452460
{
453-
return [
454-
Card::class => [Card::class],
455-
TypedCard::class => [TypedCard::class],
456-
];
461+
yield Card::class => [Card::class];
462+
yield TypedCard::class => [TypedCard::class];
463+
464+
if (class_exists(EnumType::class)) {
465+
yield CardNativeEnum::class => [CardNativeEnum::class];
466+
yield TypedCardNativeEnum::class => [TypedCardNativeEnum::class];
467+
}
457468
}
458469

459470
public function testItAllowsReadingAttributes(): void

0 commit comments

Comments
 (0)