0

ENUM types are awesome. They allow strict value restrictions and make code refactoring easy. Unfortunately, PHP not only lacks these until version 8.1, the Doctrine DBAL also lacks behind and does not offer a easy to use solution out of the box. I was looking for a solution that would allow me:

  • native ENUM type in DB
  • no magic strings in PHP
  • as little code repetition as possible
  • PHP 7.4+ (cannot use PHP 8.1)

This question is to be self-answered for those looking for such solution, because after hours of struggle, I am quite proud of what I made. See below, hope it helps:

michnovka
  • 2,880
  • 3
  • 26
  • 58
  • 2
    yes, thats feature of Stackoverflow. When you ask a question, gives you option to answer yourself directly. The purpose is that when somebody is looking for a solution, they find it. – michnovka Mar 01 '22 at 19:18

1 Answers1

0

Start by creating an abstract base class which extends Doctrine\DBAL\Types\Type. This allows it to be used as a type in Entity column declarations.

<?php

namespace App\DBAL;

use Doctrine\DBAL\Types\Type;
use Doctrine\DBAL\Platforms\AbstractPlatform;
use Exception;
use InvalidArgumentException;
use ReflectionClass;

abstract class EnumType extends Type
{
    
    private static ?array $constCacheArray = NULL;
    
    public static function getConstants() : array
    {
        if (self::$constCacheArray == NULL)
            self::$constCacheArray = [];
        
        $calledClass = get_called_class();
        
        if (!array_key_exists($calledClass, self::$constCacheArray)) {
            $reflect = new ReflectionClass($calledClass);
            self::$constCacheArray[$calledClass] = $reflect->getConstants();
        }
        
        return self::$constCacheArray[$calledClass];
    }
    
    public static function isValidName($name, $strict = false) : bool
    {
        $constants = self::getConstants();
        
        if ($strict) {
            return array_key_exists($name, $constants);
        }
        
        $keys = array_map('strtolower', array_keys($constants));
        return in_array(strtolower($name), $keys);
    }
    
    public static function isValidValue($value, $strict = true) : bool
    {
        $values = array_values(self::getConstants());
        return in_array($value, $values, $strict);
    }
    
    protected static string $name;
    
    public function getSQLDeclaration(array $column, AbstractPlatform $platform): string
    {
        $values = array_map(function ($val) {
            return "'" . $val . "'";
        }, self::getConstants());
        
        return "ENUM(" . implode(", ", $values) . ")";
    }
    
    /**
     * @param $value
     * @param AbstractPlatform $platform
     * @return mixed
     */
    public function convertToPHPValue($value, AbstractPlatform $platform)
    {
        return $value;
    }
    
    /**
     * @param $value
     * @param AbstractPlatform $platform
     * @return mixed
     */
    public function convertToDatabaseValue($value, AbstractPlatform $platform)
    {
        $this->checkValue($value);
        
        return $value;
    }
    
    /**
     * @param $value
     * @throws InvalidArgumentException
     */
    public function checkValue($value): void
    {
        if (!self::isValidValue($value)) {
            throw new InvalidArgumentException("Invalid '" . static::$name . "' value.");
        }
    }
    
    public function getName(): string
    {
        return static::$name;
    }
    
    public function requiresSQLCommentHint(AbstractPlatform $platform): bool
    {
        return true;
    }
    
    public static function getValuesArray(): array
    {
        return self::getConstants();
    }
    
    /**
     * @throws Exception
     */
    public static function getChoicesArray(): array
    {
        throw new Exception("Not implemented");
    }
}

Credit for the base of this goes to @Brian Cline

Whats important is that this class provides some helper functions with Reflection, but it also has inherited functions that allow it to be used as actual DB type. I will show you the usage with an example below.

This is how you define a new ENUM type:

<?php

namespace App\DBAL;

class AdminRoleType extends EnumType
{
    public const ADMIN = 'ROLE_ADMIN';
    public const SUPER_ADMIN = 'ROLE_SUPER_ADMIN';
    public const CSR = 'ROLE_CSR';
    public const MANAGER = 'ROLE_MANAGER';
    public const ACCOUNTING = 'ROLE_ACCOUNTING';
    
    protected static string $name = 'admin_role';
}

Pretty simple, right? This out of the box allows you to some cool things in PHP such as:

$myRole = AdminRoleType::CSR; // 'ROLE_CSR'
$isValidRole = AdminRoleType::isValidValue('ROLE_ADMIN'); // true
$isValidRole = AdminRoleType::isValidName('ADMIN'); // true

But still we did not achieve actual ENUM type in our DB table. To do this, first add the following to your config/packages/doctrine.yaml:

doctrine:
    dbal:
        mapping_types:
            enum: string
        types:
            admin_role: App\DBAL\AdminRoleType

This maps DB ENUM type to local string type (sorry, native ENUMs are not in this solution, but for PHP 8.1 could(?) be possible.)

The last step is your Entity class:

    /**
     * @ORM\Column(name="admin_role", type="admin_role")
     */
    private string $admin_role = AdminRoleType::CSR;


    public function getAdminRole(): string
    {
        return $this->admin_role;
    }
    
    /**
     * @param string $admin_role
     * @return $this
     * @throws InvalidArgumentException
     */
    public function setAdminRole(string $admin_role): self
    {
        if(!AdminRoleType::isValidValue($admin_role))
            throw new InvalidArgumentException('Invalid Admin Role');
        
        $this->admin_role = $admin_role;
    
        return $this;
    }

As you can see the code will throw an exception if you try to set some string that is not allowed value for your ENUM.

And when you do migration, the output should look like:

ALTER TABLE admin CHANGE admin_role admin_role ENUM('ROLE_ADMIN', 'ROLE_SUPER_ADMIN', 'ROLE_CSR', 'ROLE_MANAGER', 'ROLE_ACCOUNTING') NOT NULL COMMENT '(DC2Type:admin_role)'

That's it. When you work in PHP, remember to use AdminRoleType:: class instead of magic strings. If you need to add/remove item in enum, just add/remove public const from the enum class.

michnovka
  • 2,880
  • 3
  • 26
  • 58