Skip to content

Commit

Permalink
Support native enum hydration when using NEW operator
Browse files Browse the repository at this point in the history
Using the `NEW` operator with the query builder now properly converts
scalar values to native enums inside data transfer objects.
  • Loading branch information
romm committed Jul 31, 2022
1 parent 6c64bc6 commit 5d434c8
Show file tree
Hide file tree
Showing 5 changed files with 142 additions and 13 deletions.
32 changes: 23 additions & 9 deletions lib/Doctrine/ORM/Internal/Hydration/AbstractHydrator.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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;
Expand Down Expand Up @@ -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]):
Expand Down Expand Up @@ -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<BackedEnum> $enumType
*
* @return BackedEnum|array<BackedEnum>
*/
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);
}
}
5 changes: 5 additions & 0 deletions lib/Doctrine/ORM/Query/SqlWalker.php
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
<?php

declare(strict_types=1);

namespace Doctrine\Tests\Models\DataTransferObjects;

use Doctrine\Tests\Models\Enums\Unit;

final class DtoWithArrayOfEnums
{
/** @var Unit[] */
public $supportedUnits;

/**
* @param Unit[] $supportedUnits
*/
public function __construct(array $supportedUnits)
{
$this->supportedUnits = $supportedUnits;
}
}
18 changes: 18 additions & 0 deletions tests/Doctrine/Tests/Models/DataTransferObjects/DtoWithEnum.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
<?php

declare(strict_types=1);

namespace Doctrine\Tests\Models\DataTransferObjects;

use Doctrine\Tests\Models\Enums\Suit;

final class DtoWithEnum
{
/** @var Suit|null */
public $suit;

public function __construct(?Suit $suit)
{
$this->suit = $suit;
}
}
79 changes: 75 additions & 4 deletions tests/Doctrine/Tests/ORM/Functional/EnumTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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();

Expand All @@ -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')
Expand Down Expand Up @@ -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]);
Expand Down

0 comments on commit 5d434c8

Please sign in to comment.