From 5d434c89d3e4ee8df38734ba18b809ac71f2e4fb Mon Sep 17 00:00:00 2001 From: Romain Canon Date: Wed, 27 Jul 2022 09:52:23 +0200 Subject: [PATCH] Support native enum hydration when using `NEW` operator Using the `NEW` operator with the query builder now properly converts scalar values to native enums inside data transfer objects. --- .../Internal/Hydration/AbstractHydrator.php | 32 +++++--- lib/Doctrine/ORM/Query/SqlWalker.php | 5 ++ .../DtoWithArrayOfEnums.php | 21 +++++ .../DataTransferObjects/DtoWithEnum.php | 18 +++++ .../Tests/ORM/Functional/EnumTest.php | 79 ++++++++++++++++++- 5 files changed, 142 insertions(+), 13 deletions(-) create mode 100644 tests/Doctrine/Tests/Models/DataTransferObjects/DtoWithArrayOfEnums.php create mode 100644 tests/Doctrine/Tests/Models/DataTransferObjects/DtoWithEnum.php diff --git a/lib/Doctrine/ORM/Internal/Hydration/AbstractHydrator.php b/lib/Doctrine/ORM/Internal/Hydration/AbstractHydrator.php index 2115a9e819a..0c74f23a560 100644 --- a/lib/Doctrine/ORM/Internal/Hydration/AbstractHydrator.php +++ b/lib/Doctrine/ORM/Internal/Hydration/AbstractHydrator.php @@ -423,6 +423,10 @@ protected function gatherRowData(array $data, array &$id, array &$nonemptyCompon $type = $cacheKeyInfo['type']; $value = $type->convertToPHPValue($value, $this->_platform); + if ($value !== null && isset($cacheKeyInfo['enumType'])) { + $value = $this->buildEnum($value, $cacheKeyInfo['enumType']); + } + $rowData['newObjects'][$objIndex]['class'] = $cacheKeyInfo['class']; $rowData['newObjects'][$objIndex]['args'][$argIndex] = $value; break; @@ -431,16 +435,8 @@ protected function gatherRowData(array $data, array &$id, array &$nonemptyCompon $type = $cacheKeyInfo['type']; $value = $type->convertToPHPValue($value, $this->_platform); - // Reimplement ReflectionEnumProperty code if ($value !== null && isset($cacheKeyInfo['enumType'])) { - $enumType = $cacheKeyInfo['enumType']; - if (is_array($value)) { - $value = array_map(static function ($value) use ($enumType): BackedEnum { - return $enumType::from($value); - }, $value); - } else { - $value = $enumType::from($value); - } + $value = $this->buildEnum($value, $cacheKeyInfo['enumType']); } $rowData['scalars'][$fieldName] = $value; @@ -580,6 +576,7 @@ protected function hydrateColumnInfo($key) 'argIndex' => $mapping['argIndex'], 'objIndex' => $mapping['objIndex'], 'class' => new ReflectionClass($mapping['className']), + 'enumType' => $this->_rsm->enumMappings[$key] ?? null, ]; case isset($this->_rsm->scalarMappings[$key], $this->_hints[LimitSubqueryWalker::FORCE_DBAL_TYPE_CONVERSION]): @@ -688,4 +685,21 @@ protected function registerManaged(ClassMetadata $class, $entity, array $data) $this->_em->getUnitOfWork()->registerManaged($entity, $id, $data); } + + /** + * @param mixed $value + * @param class-string $enumType + * + * @return BackedEnum|array + */ + private function buildEnum($value, string $enumType) + { + if (is_array($value)) { + return array_map(static function ($value) use ($enumType): BackedEnum { + return $enumType::from($value); + }, $value); + } + + return $enumType::from($value); + } } diff --git a/lib/Doctrine/ORM/Query/SqlWalker.php b/lib/Doctrine/ORM/Query/SqlWalker.php index 7539b53ec91..ce074a4df99 100644 --- a/lib/Doctrine/ORM/Query/SqlWalker.php +++ b/lib/Doctrine/ORM/Query/SqlWalker.php @@ -1617,6 +1617,11 @@ public function walkNewObject($newObjectExpression, $newObjectResultAlias = null } $sqlSelectExpressions[] = $col . ' AS ' . $columnAlias; + + if (! empty($fieldMapping['enumType'])) { + $this->rsm->addEnumResult($columnAlias, $fieldMapping['enumType']); + } + break; case $e instanceof AST\Literal: diff --git a/tests/Doctrine/Tests/Models/DataTransferObjects/DtoWithArrayOfEnums.php b/tests/Doctrine/Tests/Models/DataTransferObjects/DtoWithArrayOfEnums.php new file mode 100644 index 00000000000..ac7a6bdefc5 --- /dev/null +++ b/tests/Doctrine/Tests/Models/DataTransferObjects/DtoWithArrayOfEnums.php @@ -0,0 +1,21 @@ +supportedUnits = $supportedUnits; + } +} diff --git a/tests/Doctrine/Tests/Models/DataTransferObjects/DtoWithEnum.php b/tests/Doctrine/Tests/Models/DataTransferObjects/DtoWithEnum.php new file mode 100644 index 00000000000..dfef95e5baf --- /dev/null +++ b/tests/Doctrine/Tests/Models/DataTransferObjects/DtoWithEnum.php @@ -0,0 +1,18 @@ +suit = $suit; + } +} diff --git a/tests/Doctrine/Tests/ORM/Functional/EnumTest.php b/tests/Doctrine/Tests/ORM/Functional/EnumTest.php index 9d1b1c92888..6837c35d832 100644 --- a/tests/Doctrine/Tests/ORM/Functional/EnumTest.php +++ b/tests/Doctrine/Tests/ORM/Functional/EnumTest.php @@ -7,7 +7,10 @@ use Doctrine\ORM\Mapping\Column; use Doctrine\ORM\Mapping\Driver\AttributeDriver; use Doctrine\ORM\Mapping\MappingException; +use Doctrine\ORM\Query\Expr\Func; use Doctrine\ORM\Tools\SchemaTool; +use Doctrine\Tests\Models\DataTransferObjects\DtoWithArrayOfEnums; +use Doctrine\Tests\Models\DataTransferObjects\DtoWithEnum; use Doctrine\Tests\Models\Enums\Card; use Doctrine\Tests\Models\Enums\CardWithDefault; use Doctrine\Tests\Models\Enums\CardWithNullable; @@ -69,11 +72,7 @@ public function testEnumHydration(): void $card = new Card(); $card->suit = Suit::Clubs; - $cardWithNullable = new CardWithNullable(); - $cardWithNullable->suit = null; - $this->_em->persist($card); - $this->_em->persist($cardWithNullable); $this->_em->flush(); $this->_em->clear(); @@ -85,6 +84,18 @@ public function testEnumHydration(): void $this->assertInstanceOf(Suit::class, $result[0]['suit']); $this->assertEquals(Suit::Clubs, $result[0]['suit']); + } + + public function testNullableEnumHydration(): void + { + $this->setUpEntitySchema([Card::class, CardWithNullable::class]); + + $cardWithNullable = new CardWithNullable(); + $cardWithNullable->suit = null; + + $this->_em->persist($cardWithNullable); + $this->_em->flush(); + $this->_em->clear(); $result = $this->_em->createQueryBuilder() ->from(CardWithNullable::class, 'c') @@ -115,6 +126,66 @@ public function testEnumArrayHydration(): void self::assertEqualsCanonicalizing([Unit::Gram, Unit::Meter], $result[0]['supportedUnits']); } + public function testEnumInDtoHydration(): void + { + $this->setUpEntitySchema([Card::class, CardWithNullable::class]); + + $card = new Card(); + $card->suit = Suit::Clubs; + + $this->_em->persist($card); + $this->_em->flush(); + $this->_em->clear(); + + $result = $this->_em->createQueryBuilder() + ->from(CardWithNullable::class, 'c') + ->select('NEW ' . DtoWithEnum::class . '(c.suit)') + ->getQuery() + ->getResult(); + + $this->assertNull($result[0]->suit); + } + + public function testNullableEnumInDtoHydration(): void + { + $this->setUpEntitySchema([Card::class, CardWithNullable::class]); + + $cardWithNullable = new CardWithNullable(); + $cardWithNullable->suit = null; + + $this->_em->persist($cardWithNullable); + $this->_em->flush(); + $this->_em->clear(); + + $result = $this->_em->createQueryBuilder() + ->from(CardWithNullable::class, 'c') + ->select('NEW ' . DtoWithEnum::class . '(c.suit)') + ->getQuery() + ->getResult(); + + $this->assertNull($result[0]->suit); + } + + public function testEnumArrayInDtoHydration(): void + { + $this->setUpEntitySchema([Scale::class]); + + $scale = new Scale(); + $scale->supportedUnits = [Unit::Gram, Unit::Meter]; + + $this->_em->persist($scale); + $this->_em->flush(); + $this->_em->clear(); + + $result = $this->_em->createQueryBuilder() + ->from(Scale::class, 's') + ->select(new Func('NEW ' . DtoWithArrayOfEnums::class, ['s.supportedUnits'])) + ->getQuery() + ->getResult(); + + self::assertEqualsCanonicalizing([Unit::Gram, Unit::Meter], $result[0]->supportedUnits); + } + public function testFindByEnum(): void { $this->setUpEntitySchema([Card::class]);