Skip to content

Commit

Permalink
Auto-detect values for EnumType columns
Browse files Browse the repository at this point in the history
  • Loading branch information
derrabus committed Oct 12, 2024
1 parent 5bfb744 commit 30b6658
Show file tree
Hide file tree
Showing 6 changed files with 101 additions and 33 deletions.
9 changes: 5 additions & 4 deletions psalm-baseline.xml
Original file line number Diff line number Diff line change
Expand Up @@ -212,14 +212,14 @@
</ParamNameMismatch>
</file>
<file src="src/Internal/Hydration/AbstractHydrator.php">
<ReferenceConstraintViolation>
<code><![CDATA[return $rowData;]]></code>
<code><![CDATA[return $rowData;]]></code>
</ReferenceConstraintViolation>
<PossiblyUndefinedArrayOffset>
<code><![CDATA[$newObject['args']]]></code>
<code><![CDATA[$newObject['args']]]></code>
</PossiblyUndefinedArrayOffset>
<ReferenceConstraintViolation>
<code><![CDATA[return $rowData;]]></code>
<code><![CDATA[return $rowData;]]></code>
</ReferenceConstraintViolation>
</file>
<file src="src/Internal/Hydration/ArrayHydrator.php">
<PossiblyInvalidArgument>
Expand Down 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
11 changes: 8 additions & 3 deletions src/Mapping/ClassMetadata.php
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@
use BackedEnum;
use BadMethodCallException;
use Doctrine\DBAL\Platforms\AbstractPlatform;
use Doctrine\DBAL\Types\EnumType;
use Doctrine\DBAL\Types\Types;
use Doctrine\Deprecations\Deprecation;
use Doctrine\Instantiator\Instantiator;
use Doctrine\Instantiator\InstantiatorInterface;
Expand All @@ -23,6 +25,7 @@
use ReflectionProperty;
use Stringable;

use function array_column;
use function array_diff;
use function array_intersect;
use function array_key_exists;
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,10 @@ protected function validateAndCompleteFieldMapping(array $mapping): FieldMapping
if (! empty($mapping->id)) {
$this->containsEnumIdentifier = true;
}

if (class_exists(EnumType::class) && $mapping->type === Types::ENUM && ! isset($mapping->options['values'])) {

Check failure on line 1237 in src/Mapping/ClassMetadata.php

View workflow job for this annotation

GitHub Actions / Static Analysis with PHPStan (3.8.2, phpstan-dbal3.neon)

Access to undefined constant Doctrine\DBAL\Types\Types::ENUM.

Check failure on line 1237 in src/Mapping/ClassMetadata.php

View workflow job for this annotation

GitHub Actions / Static Analysis with Psalm (3.8.2)

UndefinedConstant

src/Mapping/ClassMetadata.php:1237:69: UndefinedConstant: Constant Doctrine\DBAL\Types\Types::ENUM is not defined (see https://psalm.dev/020)
$mapping->options['values'] = array_column($mapping->enumType::cases(), 'value');
}
}

return $mapping;
Expand Down
45 changes: 26 additions & 19 deletions src/Mapping/DefaultTypedFieldMapper.php
Original file line number Diff line number Diff line change
Expand Up @@ -49,30 +49,37 @@ 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']) || $mapping['type'] === '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']])]
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;
}
21 changes: 14 additions & 7 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 @@ -447,13 +451,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 30b6658

Please sign in to comment.