Working with Enum Types in Symfony and PostgreSQL 

Sometimes we need a list of valid values for a certain field in the database. To this end, we can create a custom data type in Symfony and use it in our entity mapping definitions. That way, we can also use it in our validations. It’s especially helpful when using PostgreSQL ENUM types.

This procedure is intended for PHP versions earlier than PHP 8.1 which include the native Enum type. We could handle the same solution using enums with few changes or using one of another existing approach. Anyway, many projects still use PHP 8.0 or earlier and this code is still useful for customising our enum types in PostgreSQL with Doctrine and Symfony.

Imagine you have defined a type appointment_status as Enum in your old database schema like this.  

CREATE TYPE public.appointment_status AS ENUM
    ('Open', 'Invited', 'Assigned', 'Unassigned');

And you use this type in a certain column: 

CREATE TABLE IF NOT EXISTS public.appointment
    {
        id integer NOT NULL,
        status public.appointment_status NOT NULL DEFAULT 'Open'::public.appointment_status,
        comment character varying(500)
    }

So, in this article we learn how to handle this kind of data in an easy way.

Creating an Abstract Enum Type

The first step is creating an Abstract Class for this kind of type of data. With this abstraction, we can reuse the class for any enum field. 

<?php 
 
namespace App\Infrastructure\Persistence\Doctrine\DBAL; 
 
use Doctrine\DBAL\Platforms\AbstractPlatform; 
use Doctrine\DBAL\Types\Type; 
 
abstract class AbstractEnumType extends Type 
{ 
 
    protected string $schema = "public"; 
    protected string $name; 
    protected array $values = array(); 
 
 
    public function getSQLDeclaration(array $column, AbstractPlatform $platform): string 
    { 
        return $this->schema . ".\"" . $this->getName() . "\""; 
    } 
 
    public function convertToPHPValue($value, AbstractPlatform $platform) 
    { 
        return $value; 
    } 
 
    public function convertToDatabaseValue($value, AbstractPlatform $platform) 
    { 
        if (!in_array($value, $this->getValidValues())) { 
            throw new \InvalidArgumentException("Invalid '".$this->name."' value."); 
        } 
        return $value; 
    } 
 
    public function getName(): string 
    { 
        return $this->name; 
    } 
 
    public function requiresSQLCommentHint(AbstractPlatform $platform): bool 
    { 
        return true; 
    } 
 
    public function getValidValues(): array 
    { 
        return $this->values; 
    } 
 
} 

 

Creating the Enum Type

Now you must follow creating the specific Enum Type extending the Abstract Class: 

<?php 
 
namespace App\Infrastructure\Persistence\Doctrine\DBAL; 
 
class AppointmentStatusType extends AbstractEnumType 
{ 
    protected string $name = 'appointment_status'; 
    protected array $values = []; 
    protected static array $options = array( 
        'Open', 
        'Invited', 
        'Assigned', 
        'Unassigned' 
    ); 
 
    function __init() 
    { 
        $this->values = self::$options; 
    } 
 
    public function getValidValues(): array 
    { 
        return self::$options; 
    } 
} 

 

Tip: If the list is too long, you can split the class into two files; the logic on one side, and the values on the other, and get more maintainable classes  

<?php

namespace App\Infrastructure\Persistence\Doctrine\DBAL;

USE APP\Infrastructure\Persistence\Doctrine\DBAL\Enums\CitizenshipEnum;

class EmployeeCitizenshipType extends AbstractEnumType
{
    protected string $name = 'employee_citizenship';
    protected array $values = [];

    function __init()
    {
        $this->values = CitizenshipEnum::getValues();
    }

    public function getValidValues(): array
    {
        return CitizenshipEnum::getValues();
    }
}

namespace App\Infrastructure\Persistence\Doctrine\DBAL\Enums;

class CitizenshipEnum
{
    protected static array $values = [
        'Ohne Angabe',
        'Afghanistan',
        'Algerien',
        'Andorra',
        'Angola',
        '[....]',
        'Vietnam',
        'Zentralafrikanische Republik',
        'Zypern',
    ];

    public static function getValues(): array
    {
        return self::$values;
    }
}

Define as type in Doctrine

At this point we need to tell Symfony that we have a new datatype. So, the only pending thing is a map in doctrine and we have to tell it “Eh, this data type is special!”. Edit the doctrine.yml and add these lines: 

doctrine: 
    dbal: 

      […] 
      mapping_types: 
          appointment_status: appointment_status 

 

    types: 
          appointment_status: App\..\AppointmentStatusType 

 

Mapping entity with custom datatype 

Yes! Doctrine now knows the only admitted values for the status. In our XML file for mapping entities, we can now use:

<!-- status -->
<field name="status" type="appointment_status" column="status">
    <options>
        <option name="default">Open</option>
    </options>
</field> 

In our entity file, we must use “string” and don’t worry about the datatype… Doctrine does everything for us: 

private string $status; 

Using asserts to validate inputs

If we want to validate an input with special datatype now, as we saw before, we can use this constraint using Symfony/Validator. This is useful for validating forms or POST calls.

status: 
  - NotBlank: ~ 
  - Choice: { callback: [ \[..]ProductStatusType, getValidValues] } 

And Symfony will check if the given value is defined in the admits values in our custom data type…. Magic! The callback definition makes a call to method getValidValues() and it checks if the given value is a valid one.   

Symfony is amazing!