Using PHP 8.1 enumerations in a Symfony project

Published on 2022-09-03 • Modified on 2022-09-07

This post shows how to use PHP 8.1 enumerations in a Symfony project. We review the different steps to use this new PHP 8.1 useful feature in different parts of a Symfony application. Let's go! 😎

Prerequisite

I will assume you have at least a basic knowledge of Symfony and that you know what an enumeration is.

Configuration

  • PHP 8.3
  • Symfony 6.4.6
  • doctrine/orm 2.11 (at least)

Introduction

Enumeration is a long-awaited feature of PHP. It's available as of PHP 8.1. In the past, some bundles could simulate this feature. For example, I used the fresh/doctrine-enum-bundle bundle in a previous article to create an enumeration and implement a workflow. Enumerations are supported in Symfony as of the 5.4 version; check out the feature introduction on the Symfony blog.

Goal

This time, the goal is to use the PHP enumerations without a bundle or Doctrine extension. I'll take as an example the article.type field of my database, which tells the type of article we have. It can be a blog post (BLOG_POST), like this article, or it can be a snippet (SNIPPET). I used two constants in the past. Let's see how to replace this with a clean enumeration.

public const TYPE_BLOG_POST = 'blog_post';
public const TYPE_SNIPPET = 'snippet';

The PHP enumeration type

The first step is to create the enumeration type. We can put it in src/Enum:

<?php

declare(strict_types=1);

namespace App\Enum;

enum ArticleType: string
{
    case BLOG_POST = 'blog_post';
    case SNIPPET = 'snippet';

    /**
     * @return array<string,string>
     */
    public static function getAsArray(): array
    {
        return array_reduce(
            self::cases(),
            static fn (array $choices, ArticleType $type) => $choices + [$type->name => $type->value],
            [],
        );
    }
}

It's a backed enumeration. It means that a scalar type represents each item of the enumeration; in this case, it's associated with a string value, as shown by the : string typehint. I had constants before to describe the two values, so I took the same case, but we can use camelcase, which is probably better. The getAsArray() function will be useful later in the article.

Now we must tell Doctrine to use the new enumeration we've just created.

Doctrine entity

Let's see the new property declaration:

    #[ORM\Column(name: 'type', type: 'string', nullable: false, enumType: ArticleType::class)]
    #[Groups(groups: [Article::GROUP_DEFAULT])]
    // protected string $type = ArticleType::TYPE_BLOG_POST;
    protected ArticleType $type = ArticleType::BLOG_POST;

We can now typehint the property with our new enumeration type. I've kept the old commented declaration; it was a string with a default value coming from one of the two available constants. Now we also use a default value of the enumeration. The field is not nullable, and the attribute now has an extra option: , enumType: ArticleType::class which is required for this property type. It was introduced in doctrine/orm 2.11.

We must modify the getter/setter accordingly:

    public function getType(): ArticleType
    {
        return $this->type;
    }

    public function setType(ArticleType $type): self
    {
        $this->type = $type;

        return $this;
    }

We can regenerate the schema. Note that with this setup, the field in your database is a string and not an actual database enumeration we can have with MySQL or PostgreSQL. Now, let's see how to retrieve data with this new type.

Doctrine repository

We can modify a repository method to pass the enumeration type instead of a string. Nothing to change in the query builder in this case; we can directly give the enumeration value to the setParameter() function: Doctrine handles this for us now.

    public function findForLangQuery(ArticleType $type, string $lang, ?string $keyword = null, ?int $maxResults = null): Query
    {
        $qb = $this->createQueryBuilder('a')
            ->where('a.type = :type')
            ->setParameter('type', $type)
            ->andWhere('a.inLanguage LIKE :inLanguage')
            ->andWhere('a.active = true')
            ->setParameter('inLanguage', '%'.$lang.'%')
            ->addOrderBy('a.datePublished', 'desc');

If you've got this kind of error:

Cannot assign string to property App\Entity\Article::$type of type App\Enum\ArticleType

Be sure to clear all your Doctrine cache and restart your Docker containers.

Fixtures

I use the hautelook/alice-bundle to load the fixtures. Before, I could directly use the constants. Now, we can also use the enumerations cases as constants (the nelmio/alice bundle was updated to support this):

App\Entity\Article:
  article (template):
    #type: !php/const App\DBAL\Types\ArticleType::TYPE_BLOG_POST
    type: !php/const App\Enum\ArticleType::BLOG_POST

Forms

Of course, we can use the enumeration in the form, thanks to the new EnumType. Here is a simple example:

<?php

declare(strict_types=1);

namespace App\Form;

use App\Enum\ArticleType;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\Extension\Core\Type\CheckboxType;
use Symfony\Component\Form\Extension\Core\Type\EnumType;
use Symfony\Component\Form\Extension\Core\Type\TextType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\Translation\TranslatableMessage;

/**
 * Example form for the nΒ°216 blog post.
 */
final class ArticleCreateType extends AbstractType
{
    public function buildForm(FormBuilderInterface $builder, array $options): void
    {
        $builder->add('type', EnumType::class, [
            'class' => ArticleType::class,
            'choice_label' => fn (ArticleType $choice) => new TranslatableMessage('article_type.'.$choice->name, [], 'post_216'),
        ]);
        $builder->add('name', TextType::class);
        $builder->add('active', CheckboxType::class);
    }
}

We use the EnumType, and we have to pass the fully-qualified class of the enumeration to the class option. It extends the ChoiceType; thus, we can use the choice_label option to customize each value's associated label. Let's render it:

It's rendered as a simple drop-down list.

EasyAdmin

We can also use enums in EasyAdmin, but there is still some work until it's perfectly integrated into the bundle. Check out the open PRs (and feel free to contribute, of course). For now, here is what I do:

        $type = ChoiceField::new('type')
            ->setFormType(EnumType::class)
            ->setFormTypeOption('class', ArticleType::class)
            ->setChoices(ArticleType::getAsArray())
        ;

        if (Crud::PAGE_INDEX === $pageName) {
            return [$id, $type, $publisher, $author, $inLanguage, $name, $keyword, $datePublished, $dateModified];
        }

        $type->setChoices(ArticleType::cases());

For the list, we have to use the setChoices() function with a callback (the getAsArray() function we saw before in the enumeration); if not, we have the following error:

Warning: array_flip(): Can only flip string and integer values, entry skipped

We can use the ArticleType::cases() to set the choices for the create and edit views Once again, it's just a matter of time until we don't have to use the first trick. I'll update the blog post when everything is OK from this point of view.

API Platform

What about API Platform? If we try to submit a wrong value:

curl -X 'POST' \
'https://127.0.0.1:8000/api/articles' \
-H 'accept: application/ld+json' \
-H 'Content-Type: application/ld+json' \
-d '{
    "type": "foobar",
    ...
}'

We've got a bad request (400) error:

{
    "@context": "/api/contexts/Error",
    "@type": "hydra:Error",
    "hydra:title": "An error occurred",
    "hydra:description": "The data must belong to a backed enumeration of type App\\Enum\\ArticleType"
}

Perfect! Nothing to do; everything is already correctly handled. πŸ˜€

Conclusion

We had an overview of the usage of PHP 8.1 enumerations in a Symfony project. As you can see, there's a little more work than using raw constants. It is worth it, we don't have to use an extra bundle or Doctrine extension anymore, and the code is much more straightforward and clean. But, above all, we had the pleasure to use a new PHP fonctionality, showing that is it very far from being dead 🐘.

That's it! I hope you like it. Check out the links below to have additional information related to the post. As always, feedback, likes and retweets are welcome. (see the box below) See you! COil. 😊

  Read the doc   Read the doc  More on Stackoverflow

They gave feedback and helped me to fix errors and typos in this article; many thanks to JuGa . πŸ‘

  Work with me!


Call to action

Did you like this post? You can help me back in several ways: (use the Tweet on the right to comment or to contact me )

Thank you for reading! And see you soon on Strangebuzz! πŸ˜‰

COil