Utilisation des énumérations PHP 8.1 dans un projet Symfony

Publié le 03/09/2022 • Actualisé le 07/09/2022

Dans cet article, nous voyons comment utiliser des énumérations PHP 8.1 dans un projet Symfony. Nous allons passer en revue les étapes pour utiliser cette intéressante nouvelle fonctionnalité dans les différentes parties d'une application Symfony. C'est parti ! 😎


English language detected! 🇬🇧

  We noticed that your browser is using English. Do you want to read this post in this language?

Read the english version 🇬🇧 Close

Prérequis

Je présumerai que vous avez au moins les connaissances de base de Symfony et que vous savez ce qu'est une énumération.

Configuration

  • PHP 8.3
  • Symfony 6.4.6
  • doctrine/orm 2.11 (au moins)

Introduction

Les énumérations sont une fonctionnalité attendue depuis longtemps dans PHP. Elles sont disponibles depuis PHP 8.1. Par le passé, quelques bundles permettaient de les simuler. Par exemple, j'ai utilisé le bundle fresh/doctrine-enum-bundle dans un précédent article pour créer une énumération et implémenter un workflow. Les énumérations sont supportées depuis Symfony 5.4 ; jetez un coup d'œil à l'article d'introduction sur le blog Symfony.

But

Cette fois, le but est d'utiliser les énumérations sans bundle ni extension Doctrine. Je prendrai pour exemple le champ article.type de ma base de données qui indique à quel type d'article, nous avons à faire. Ça peut être un article de blog (BLOG_POST) comme cet article, ou ça peut être un snippet (SNIPPET). Par le passé, j'utilisais deux constantes. Voyons comment les remplacer avec une énumération propre.

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

Le type énumération PHP

La première étape est de créer le nouveau type énumération. On peut le mettre dans 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],
            [],
        );
    }
}

C'est une backed enumeration. Cela signifie que l'on a un type scalaire représentant chaque valeur de l'énumération ; dans ce cas, elles sont associées à une chaîne, comme indiqué par le type de retour : string. Avant, j'avais deux constantes pour décrire les deux valeurs, donc j'ai pris la même casse; mais on peut utiliser le camelCase, qui est probablement plus adapté ici. La fonction getAsArray() sera utile un peu plus tard dans cet article.

Maintenant, on doit indiquer à Doctrine d'utiliser l'énumération que nous venons de créer.

L'entité Doctrine

Voyons la nouvelle déclaration de la propriété :

    #[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;

On peut maintenant lui associer le type de notre nouvelle énumération. J'ai gardé l'ancienne déclaration commentée; c'était une chaîne avec une valeur par défaut provenant de l'une de mes deux constantes de type disponible. On utilise aussi un type par défaut pour notre énumération. Le champ n'est pas nullable, et l'attribut à une nouvelle option : , enumType: ArticleType::class qui est requise pour ce type de propriété. Il a été introduit dans doctrine/orm 2.11.

On doit modifier les getters et setters en conséquence :

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

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

        return $this;
    }

On peut régénérer le schéma de base de données. Veuillez noter qu'avec ces paramètres, le champ dans la base de données est une chaîne et non une vraie énumération comme on peut le faire avec MySQL ou PostgreSQL. Ça permet d'être plus souple et efficace quand on a besoin de faire une migration, mais ça laisse aussi la possibilité de mettre en base une valeur ne faisant pas partie de l'énumération (insertion directe en base). Maintenant, voyons comment récupérer des données avec ce nouveau type.

Dépôt Doctrine

On peut modifier les méthodes du dépôt pour passer le type énumération au lieu d'une chaîne. Rien à changer dans le constructeur de requêtes dans ce cas ; on peut directement donner la valeur de l'énumération à la fonction setParameter() : Doctrine gère la conversion pour nous.

    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');

Si vous avez ce type d'erreur :

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

Soyez sûr de supprimer tout votre cache Doctrine et de redémarrer vos conteneurs Docker.

Données factices

J'utilise le bundle hautelook/alice-bundle pour charger des données factices (fixtures). Avant, je pouvais directement utiliser les deux constantes. Maintenant, on peut aussi utiliser les cas des énumerations en tant que constantes (le bundle nelmio/alice a été mis à jour pour gérer ceci) :

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

Les formulaires

Bien sûr, on peut utiliser les énumérations dans les formulaires grâce au nouveau EnumType. Voici un exemple simple :

<?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);
    }
}

On utilise le type EnumType et l'on passe la FQCN de l'énumération à l'option class du type. Il étend le type ChoiceType, donc, on peut utiliser l'option choice_label pour personnaliser le libellé de chaque valeur. Affichons-le :

Les valeurs sont ici affichées en tant que simple liste déroulante.

EasyAdmin

On peut aussi utiliser les énumérations dans EasyAdmin, mais il y a encore un peu de travail pour qu'elles soient parfaitement intégrées dans le bundle. Jetez un coup d'œil aux PRs ouvertes (et n'hésitez pas à contribuer bien sûr). Pour l'instant, voilà ce que je fais :

        $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());

Pour la liste, on doit utiliser la méthode setChoices() avec une fonction de rappel ; (la fonction getAsArray() que nous avons vue auparavant dans l'énumération) sinon, on a l'erreur suivante :

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

On peut utiliser plus simplement ArticleType::cases() pour indiquer les choix pour les vues de création et d'édition. Encore une fois, c'est juste une histoire de temps avant que l'on ait plus à utiliser la première astuce. Je mettrai à jour cet article une fois le problème réglé.

API Platform

Quid d'API Platform ? Si l'on essaie de soumettre une mauvaise valeur :

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",
    ...
}'

On a une erreur "mauvaise requête" (400) :

{
    "@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"
}

Parfait ! Rien à faire, tout est déjà géré correctement ! 😀

Conclusion

Nous avons eu une vue d'ensemble de l'utilisation des énumérations PHP dans un projet Symfony. Comme vous pouvez le voir, il y a un peu plus de travail que d'utiliser des constantes brutes. Ça vaut le coup, on n'a plus à utiliser de bundle additionnel ni d'extension Doctrine. Le code est plus concis et propre. Mais surtout, nous avons le plaisir d'utiliser une nouvelle fonctionnalité de PHP qui montre qu'il est très loin d'être mort 🐘.

Et voilà ! J'espère que vous avez aimé. Découvrez d'autres informations en rapport à cet article avec les liens ci-dessous. Comme toujours, retours, likes et retweets sont les bienvenus. (voir la boîte ci-dessous) À tantôt ! COil. 😊

  Lire la doc   Lire la doc  Plus sur Stackoverflow

Ils m'ont donné leurs retours et m'ont aidé à corriger des erreurs et typos dans cet article, un grand merci à : JuGa . 👍

  Travaillez avec moi !


A vous de jouer !

Ces articles vous ont été utiles ? Vous pouvez m'aider à votre tour de plusieurs manières : (cf le tweet à droite pour me contacter )

  • Me remonter des erreurs ou typos.
  • Me remonter des choses qui pourraient être améliorées.
  • Aimez et retweetez !
  • Suivez moi sur Twitter
  • Inscrivez-vous au flux RSS.
  • Cliquez sur les boutons Plus sur Stackoverflow pour me faire gagner des badges "annonceur" 🏅.

Merci d'avoir tenu jusque ici et à très bientôt sur Strangebuzz ! 😉

COil