PHP enum as ENUM

PHP enum as ENUM

Ufo\DoctrineHelper\DBAL\AbstractEnumType is a base Doctrine DBAL type that stores PHP backed enum values in a MySQL ENUM(...) column. The list of allowed column values is taken from the enum cases, so the schema and the code do not drift apart.

Setup

1. Enum

namespace App;

enum MyEnum: string
{
    case CASE_1 = 'My case 1';
    case CASE_2 = 'My case 2';
}

2. DBAL type

Create a class extending AbstractEnumType and set your enum class in the $enum property:

namespace App\DBAL;

use App\MyEnum;
use BackedEnum;
use Ufo\DoctrineHelper\DBAL\AbstractEnumType;

class MyEnumType extends AbstractEnumType
{
    protected string|BackedEnum $enum = MyEnum::class;
}

3. Type registration

The type name is the fully qualified enum class name (returned by getName()):

# config/packages/doctrine.yaml
doctrine:
    dbal:
        types:
            App\MyEnum: 'App\DBAL\MyEnumType'
        mapping_types:
            enum: string

mapping_types: enum: string is needed so that Doctrine can read existing ENUM columns when comparing the schema (migrations, doctrine:schema:update).

4. Entity column

namespace App\Entity;

use App\MyEnum;
use Doctrine\ORM\Mapping as ORM;

#[ORM\Entity]
#[ORM\Table(name: 'demo')]
class Demo
{
    #[ORM\Column(type: MyEnum::class, nullable: false)]
    protected string $case;

    public function __construct(MyEnum $case)
    {
        $this->case = $case->value;
    }

    public function changeCase(MyEnum $case): void
    {
        $this->case = $case->value;
    }

    public function getCase(): MyEnum
    {
        return MyEnum::from($this->case);
    }
}

The column type in the schema is ENUM('My case 1', 'My case 2').

How the type works

Method Behaviour
getSQLDeclaration() ENUM('<value>', ...) from $enum::cases()
getName() the enum class, e.g. App\MyEnum
convertToDatabaseValue() checks the value with $enum::from($value) and returns ->value; an invalid value throws InvalidArgumentException with the message Invalid 'App\MyEnum' value.
convertToPHPValue() returns the database value unchanged (string/int), not an enum instance
requiresSQLCommentHint() true — in DBAL 3 the type is marked with a column comment so that schema tools recognize it

Hence the usage rules:

  • store the scalar value ($case->value) in the entity property, not the enum instance: from() does not accept an enum, and writing fails with InvalidArgumentException;
  • convert to the enum in the getter (MyEnum::from($this->case));
  • only backed enums work (enum X: string or : int);
  • the ENUM(...) declaration does not depend on the platform — the type is meant for MySQL / MariaDB;
  • null in a nullable column is not supported — see Known issues.

Example: EAV Bundle

doctrine-eav-bundle uses AbstractEnumType for the attribute value type:

namespace Ufo\EAV\DBAL;

use BackedEnum;
use Ufo\DoctrineHelper\DBAL\AbstractEnumType;
use Ufo\EAV\Utils\DiscriminatorType;

class DiscriminatorTypeEnumType extends AbstractEnumType
{
    protected string|BackedEnum $enum = DiscriminatorType::class;
}

The bundle registers the type not in YAML but from its container extension (PrependExtensionInterface), which is convenient when the type is needed by a library rather than by the application (an excerpt of UfoEAVExtension::prepend(), shortened):

public function prepend(ContainerBuilder $container)
{
    $container->loadFromExtension('doctrine', [
        'dbal' => [
            'types' => [
                DiscriminatorType::class => DiscriminatorTypeEnumType::class,
            ],
            'mapping_types' => [
                'enum' => 'string',
            ],
        ],
    ]);
}

The entity column:

#[ORM\Column(type: DiscriminatorType::class, length: 255, nullable: true)]