From 13d9de5bd9e55cd3af279c0f815e5beec8a7d7f9 Mon Sep 17 00:00:00 2001 From: "Alexander M. Turek" Date: Sat, 12 Oct 2024 14:56:07 +0200 Subject: [PATCH] Auto-detect values for EnumType columns --- psalm-baseline.xml | 9 ++-- src/Mapping/ClassMetadata.php | 15 ++++-- src/Mapping/DefaultTypedFieldMapper.php | 49 ++++++++++++------- tests/Tests/Models/Enums/CardNativeEnum.php | 25 ++++++++++ .../Models/Enums/TypedCardNativeEnum.php | 23 +++++++++ tests/Tests/ORM/Functional/EnumTest.php | 29 +++++++---- 6 files changed, 115 insertions(+), 35 deletions(-) create mode 100644 tests/Tests/Models/Enums/CardNativeEnum.php create mode 100644 tests/Tests/Models/Enums/TypedCardNativeEnum.php diff --git a/psalm-baseline.xml b/psalm-baseline.xml index b83ae43a889..a284dd38e2d 100644 --- a/psalm-baseline.xml +++ b/psalm-baseline.xml @@ -212,14 +212,14 @@ - - - - + + + + @@ -402,6 +402,7 @@ + diff --git a/src/Mapping/ClassMetadata.php b/src/Mapping/ClassMetadata.php index 70d3ea7042f..7c9020805a5 100644 --- a/src/Mapping/ClassMetadata.php +++ b/src/Mapping/ClassMetadata.php @@ -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; @@ -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; @@ -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; @@ -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); } /** @@ -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; diff --git a/src/Mapping/DefaultTypedFieldMapper.php b/src/Mapping/DefaultTypedFieldMapper.php index 49144b8b7c8..40b37b8c426 100644 --- a/src/Mapping/DefaultTypedFieldMapper.php +++ b/src/Mapping/DefaultTypedFieldMapper.php @@ -16,6 +16,7 @@ use function array_merge; use function assert; +use function defined; use function enum_exists; use function is_a; @@ -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; diff --git a/tests/Tests/Models/Enums/CardNativeEnum.php b/tests/Tests/Models/Enums/CardNativeEnum.php new file mode 100644 index 00000000000..83d4d59a778 --- /dev/null +++ b/tests/Tests/Models/Enums/CardNativeEnum.php @@ -0,0 +1,25 @@ + ['H', 'D', 'C', 'S', 'Z']])] + public $suit; +} diff --git a/tests/Tests/Models/Enums/TypedCardNativeEnum.php b/tests/Tests/Models/Enums/TypedCardNativeEnum.php new file mode 100644 index 00000000000..59e4eb00e55 --- /dev/null +++ b/tests/Tests/Models/Enums/TypedCardNativeEnum.php @@ -0,0 +1,23 @@ +_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); @@ -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(); @@ -429,7 +437,7 @@ 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], ); @@ -437,7 +445,7 @@ public function testEnumWithNonMatchingDatabaseValueThrowsException(string $card $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 , @@ -447,13 +455,16 @@ public function testEnumWithNonMatchingDatabaseValueThrowsException(string $card $this->_em->find($cardClass, $card->id); } - /** @return array */ - public static function provideCardClasses(): array + /** @return iterable */ + 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