Skip to content

Use actual MySQL ENUM type with Doctrine DBAL #178

Closed
@michnovka

Description

Hi, thanks so much for your package. I do not have time to actually make a PR at the moment, but I wanted to share what I did to support native MySQL ENUM types.

I have my own AbstractEnumType class:

<?php

namespace App\DBAL;

use BackedEnum;
use Doctrine\DBAL\Platforms\AbstractPlatform;
use Doctrine\DBAL\Types\Type;

abstract class AbstractEnumType extends Type
{
    abstract public static function getEnumsClass(): string;

    public function getName(): string // the name of the type.
    {
        return static::getEnumsClass();
    }

    public function getSQLDeclaration(array $fieldDeclaration, AbstractPlatform $platform): mixed
    {
        /** @var BackedEnum $class */
        $class = static::getEnumsClass();

        $values = array_map(
        /** @var BackedEnum $val */
            function ($val) {
                return "'" . $val->value . "'";
            },
            $class::cases()
        );

        return "ENUM(" . implode(", ", $values) . ")";
    }

    public function convertToDatabaseValue($value, AbstractPlatform $platform): mixed
    {
        if ($value instanceof \BackedEnum) {
            return $value->value;
        }
        return null;
    }

    public function convertToPHPValue($value, AbstractPlatform $platform): mixed
    {
        if (false === enum_exists($this->getEnumsClass(), true)) {
            throw new \LogicException("This class should be an enum");
        }

        /** @var BackedEnum $class */
        $class = static::getEnumsClass();

        return $class::tryFrom($value);
    }


    /**
     * @codeCoverageIgnore
     */
    public function requiresSQLCommentHint(AbstractPlatform $platform): bool
    {
        return true;
    }
}

When I make new enum like

<?php

namespace App\Entity\Enum;

use Elao\Enum\Attribute\EnumCase;
use Elao\Enum\ReadableEnumInterface;
use Elao\Enum\ReadableEnumTrait;

enum AdminRole: string implements ReadableEnumInterface
{
    use ReadableEnumTrait;

    #[EnumCase('Admin')]
    case ADMIN = 'ROLE_ADMIN';

    #[EnumCase('Super Admin')]
    case SUPER_ADMIN = 'ROLE_SUPER_ADMIN';

    #[EnumCase('CSR')]
    case CSR = 'ROLE_CSR';

    #[EnumCase('Manager')]
    case MANAGER = 'ROLE_MANAGER';

    #[EnumCase('Accounting')]
    case ACCOUNTING = 'ROLE_ACCOUNTING';

    #[EnumCase('SEO')]
    case SEO = 'ROLE_SEO';
}

I then make also a "wrapper" in DBAL folder:

<?php

namespace App\DBAL;

use App\Entity\Enum\AdminRole;

class AdminRoleType extends AbstractEnumType
{
    public static function getEnumsClass(): string // the enums class to convert
    {
        return AdminRole::class;
    }
}

This wrapper just returns the PHP enum which it links.

In config/services.yaml I put:

services:
   _instanceof:
        App\DBAL\AbstractEnumType:
          tags: ['app.doctrine_enum_type']

and then in Kernel.php, I register all "wrapper" classes as valid types:

<?php

namespace App;

use App\DBAL\AbstractEnumType;
use Symfony\Bundle\FrameworkBundle\Kernel\MicroKernelTrait;
use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface;
use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\HttpKernel\Kernel as BaseKernel;

class Kernel extends BaseKernel implements CompilerPassInterface
{
    use MicroKernelTrait;

    public function process(ContainerBuilder $container): void
    {
        $typesDefinition = [];
        if ($container->hasParameter('doctrine.dbal.connection_factory.types')) {
            $typesDefinition = $container->getParameter('doctrine.dbal.connection_factory.types');
        }
        $taggedEnums = $container->findTaggedServiceIds('app.doctrine_enum_type');

        foreach ($taggedEnums as $enumType => $definition) {
            /** @var $enumType AbstractEnumType */
            $typesDefinition[$enumType::getEnumsClass()] = ['class' => $enumType];
        }

        $container->setParameter('doctrine.dbal.connection_factory.types', $typesDefinition);
    }

}

And thats it!. In Entity I use:

    #[ORM\Column(type: AdminRole::class)]
    private AdminRole $admin_role = AdminRole::CSR;

and the SQL generated by Doctrine migrations is:

CREATE TABLE admin (id INT AUTO_INCREMENT NOT NULL, email VARCHAR(180) NOT NULL, admin_role ENUM('ROLE_ADMIN', 'ROLE_SUPER_ADMIN', 'ROLE_CSR', 'ROLE_MANAGER', 'ROLE_ACCOUNTING', 'ROLE_SEO') NOT NULL COMMENT '(DC2Type:App\\Entity\\Enum\\AdminRole)', password VARCHAR(255) NOT NULL, token VARCHAR(255) DEFAULT NULL, UNIQUE INDEX UNIQ_880E0D76E7927C74 (email), PRIMARY KEY(id)) DEFAULT CHARACTER SET utf8mb4 COLLATE `utf8mb4_unicode_ci` ENGINE = InnoDB

Adding new native ENUM type consists of only creating the php enum, then making the wrapper class.

So I was thinking this might be interesting to extend in your own AbstractEnumType doctrine bundle, which now uses either INT or VARCHAR. And if nothing else, it might help others who want native MySQL ENUMs

Thanks for your work!

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Assignees

No one assigned

    Labels

    Type

    No type

    Projects

    No projects

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions