Guides Tech

Enrichir les Enums PHP avec des attributs : Guide complet Laravel

Les Enums en PHP offrent de nombreuses possibilités sur un projet, particulièrement lorsqu’on les associe efficacement à des entités pour gérer des affichages de valeurs, des formulaires, et plus encore. Dans ce guide complet Laravel, nous allons découvrir comment les utiliser.

Attention : les attributs PHP sont disponibles depuis la version 8 de PHP.

Cependant, il peut arriver que l’on ait besoin d’ajouter des informations supplémentaires à un Enum. Voici quelques exemples :

  • J’ai un Enum pour des rôles, mais je veux aussi gérer un texte détaillé qui va s’afficher pour chaque rôle.
  • J’ai un Enum pour gérer les états d’articles sur un blog, mais je veux également gérer des couleurs correspondantes à chaque état.

Et on peut imaginer beaucoup de cas comme ceux-ci. Une solution assez simple est de créer une méthode à notre Enum (Par exemple description) qui nous renvoie la valeur correspondante.

Cependant, cette approche peut générer de nombreuses méthodes à créer en fonction des besoins, et le code reste assez rigide, comme l’exemple suivant :

public function description(): string
{
    return match($this) 
    {
        self::ADMIN => 'He\'s got full powers',
        self::USER => 'A classic user, with classic rights',
        self::GUEST => 'Oh, a guest, be nice to him!',
    };
}

Dans cet article, nous allons voir comment aller plus loin en utilisant les Attributs PHP pour enrichir nos Enums. Nous aborderons les concepts d’Enums, d’Attributs et de Reflection API. J’ai eu cette idée en découvrant un package qui propose de nombreuses fonctionnalités supplémentaires pour les Enums, notamment les attributs. Je me suis dit que creuser ce sujet et refaire une version moi-même serait intéressant, et je vous parlerai du package en question à la fin de l’article.

Vous êtes développeur ou développeuse PHP ?
👉🏼 Découvrez nos offres d’emploi PHP
👉🏼 Testez vous sur notre QCM Laravel

Mise en place du projet

Personnellement, j’utilise Sail, qui est très simple à mettre en place, pour faire tourner mon projet en local. Toutes les informations sont sur la documentation officielle de Laravel : https://laravel.com/docs/10.x#laravel-and-docker.

J’ai également configuré un alias pour la commande sail pour ne pas avoir à écrire ./vendor/bin/sail à chaque commande. Toutes les informations se trouvent ici : https://laravel.com/docs/10.x/sail#configuring-a-shell-alias

Commençons par créer un nouveau projet avec la commande suivante :

curl -s "https://laravel.build/enums-powers" | bash

Ensuite, installons Tailwind pour pouvoir styliser rapidement notre projet (et aussi parce que j’aime bien Tailwind). Le guide complet d’installation de Tailwind pour Laravel est disponible ici : https://tailwindcss.com/docs/guides/laravel

L’installation a dû générer deux fichiers tailwind.config.js et postcss.config.js.

Une fois le projet initialisé, lancé en local et la commande npm run dev exécutée, nous pouvons commencer à travailler dessus.

Pour le moment, nous retrouvons cette fameuse landing page :

Mais elle ne nous intéresse pas, donc on peut supprimer la vue welcome.blade.php

Maintenant, on code !

Il nous faut des utilisateurs

Commençons par exécuter les migrations pour créer la table des utilisateurs :

sail artisan migrate

Ensuite, modifions le fichier database/seeders/DatabaseSeeder.php pour ajouter des utilisateurs, comme suit :

<?php

namespace Database\Seeders;

// use Illuminate\Database\Console\Seeds\WithoutModelEvents;
use Illuminate\Database\Seeder;

class DatabaseSeeder extends Seeder
{
    /**
     * Seed the application's database.
     */    public function run(): void
    {
        \App\Models\User::factory(10)->create();
    }
}

Puis on finit par lancer la commande suivante pour créer les users :

sail artisan db:seed

Création de la page de listing des utilisateurs

Commençons par créer un Controller pour gérer nos utilisateurs. Pour cela, on va exécuter la commande suivante :

sail artisan make:controller UserController

Ensuite, créons une route pour accéder à notre page de liste des utilisateurs. Modifiez le fichier routes/web.php et ajoutez la route suivante à la place de la route par défaut :

Route::get('/', [UserController::class, 'index'])->name('users.index');

Ensuite, créons la méthode index dans notre Controller pour retourner la vue de listing des utilisateurs. Pour cela, on va modifier le fichier app/Http/Controllers/UserController.php et ajouter la méthode suivante :

public function index()
{
    return view('users');
}

Puis, créons un layout pour notre application dans un fichier resources/views/components/layout.blade.php et ajoutons-y le code suivant :

<!DOCTYPE html>
<html lang="{{ str_replace('_', '-', app()->getLocale()) }}">
    <head>
        <meta charset="utf-8">
        <meta name="viewport" content="width=device-width, initial-scale=1">
        @vite('resources/css/app.css')

        <title>Enums powers</title>
        
    </head>
    <body class="antialiased">
        <div class="relative min-h-screen bg-gray-100 bg-center">
            <div class="p-6 mx-auto max-w-7xl lg:p-8">
                {{ $slot }}
            </div>
        </div>
    </body>
</html>

Ainsi, le code sur la page de listing des utilisateurs sera plus léger. On va donc créer la vue de listing des utilisateurs dans un fichier resources/views/users.blade.php et y ajouter le code suivant :

<x-layout>
   <h1 class="text-3xl font-bold">Users</h1>
   <!-- We will insert the users list here later -->
</x-layout>

Pour pouvoir afficher la liste des utilisateurs, on va l’envoyer à la vue depuis le Controller. Pour cela, modifions la méthode index du Controller app/Http/Controllers/UserController.php et ajoutons ceci :

public function index()
{
    $users = User::all();
    return view('users', compact('users'));
}

On va ensuite modifier la vue resources/views/users.blade.php pour afficher la liste des utilisateurs comme ceci :

<x-layout>
   <h1 class="text-3xl font-bold">Users</h1>
    <ul>
        @foreach ($users as $user)
            <li class="px-4 py-2 my-4 bg-white border border-gray-300 rounded">
                <p class="font-semibold">{{ $user->name }}</p>
                <p class=text-gray-500>{{ $user->email }}</p>
            </li>
        @endforeach
    </ul>
</x-layout>

Ce qui nous donne une belle liste d’utilisateurs :

Création de l’Enum pour les rôles

Nous n’avons toujours pas de rôles à afficher pour nos utilisateurs. On va donc créer un Enum pour gérer les rôles. Pour cela, on va créer le fichier app/Enums/RoleEnum.php et y ajouter le code suivant :

<?php

namespace App\Enums;

enum RoleEnum: string
{
    case ADMIN = 'admin';
    case USER = 'user';
    case GUEST = 'guest';
}

Modifions maintenant le modèle app/Models/User.php pour utiliser notre Enum. Pour cela, on va ajouter la ligne suivante dans la propriété $casts :

protected $casts = [
    /* ... */    'role' => RoleEnum::class,
];

On va ensuite modifier la migration pour rajouter la colonne roledans la table users. Pour cela, modifions le fichier database/migrations/2014_10_12_000000_create_users_table.phpavec une nouvelle définition de la table users comme ci-dessous :

Schema::create('users', function (Blueprint $table) {
    $table->id();
    $table->string('name');
    $table->string('email')->unique();
    $table->timestamp('email_verified_at')->nullable();
    $table->string('password');
    $table->string('role')->default(RoleEnum::GUEST->value);
    $table->rememberToken();
    $table->timestamps();
});

Il faut aussi modifier la factory database/factories/UserFactory.php pour y ajouter le rôle. Et transformer la définition comme-ceci :

return [
    'name' => fake()->name(),
    'email' => fake()->unique()->safeEmail(),
    'email_verified_at' => now(),
    'password' => '$2y$10$92IXUNpkjO0rOQ5byMi.Ye4oKoEa3Ro9llC/.og/at2.uheWG/igi',
    'remember_token' => Str::random(10),
    'role' => fake()->randomElement(RoleEnum::cases())->value,
];

Ce que nous avons fait ici, c’est récupérer les différents cas du RoleEnum et prendre au hasard la valeur d’un des cas pour créer le User en cours de seeding.

On modifie ensuite la vue resources/views/users.blade.php pour afficher le rôle de chaque utilisateur :

<x-layout>
   <h1 class="text-3xl font-bold">Users</h1>
    <ul>
        @foreach ($users as $user)
            <li class="flex justify-between px-4 py-2 my-4 bg-white border border-gray-300 rounded">
                <div>
                    <p class="font-semibold">{{ $user->name }}</p>
                    <p class=text-gray-500>{{ $user->email }}</p>
                </div>
                <p class="font-bold text-gray-500">{{ $user->role }}</p>
            </li>
        @endforeach
    </ul>
</x-layout>

Ici, on a rajouté le rôle et utilisé la classe Tailwind flex pour réorganiser les éléments.

Relançons les migrations et le seeding pour avoir des utilisateurs avec des rôles :

sail artisan migrate:fresh --seed

Ce qui nous donne cette nouvelle vue :

A présent, allons plus loin pour récupérer plus d’informations de l’Enum.

Modification de l’Enum (Version 1)

Admettons que je veuille ajouter une description pour chaque rôle, on va tenter de le faire sans les attributs dans un premier temps. On va donc modifier l’Enum pour y ajouter une méthode description comme ceci :

<?php

namespace App\Enums;

enum RoleEnum: string
{
    case ADMIN = 'admin';
    case USER = 'user';
    case GUEST = 'guest';

    public function description(): string
    {
        return match($this) 
        {
            self::ADMIN => 'He\'s got full powers',
            self::USER => 'A classic user, with classic rights',
            self::GUEST => 'Oh, a guest, be nice to him!',
        };
    }
}

On va pouvoir modifier la vue resources/views/users.blade.php pour afficher la description du rôle comme ceci :

<x-layout>
    <h1 class="text-3xl font-bold">Users</h1>
     <ul>
         @foreach ($users as $user)
             <li class="flex justify-between px-4 py-2 my-4 bg-white border border-gray-300 rounded">
                 <div>
                     <p class="font-semibold">{{ $user->name }}</p>
                     <p class=text-gray-500>{{ $user->email }}</p>
                 </div>
                 <div class="text-right">
                     <p class="font-bold text-gray-500">{{ $user->role }}</p>
                     <p>{{ $user->role->description() }}</p>
                 </div>
             </li>
         @endforeach
     </ul>
 </x-layout>

 

Et voilà le résultat :

Ce n’est pas la seule manière de faire ça à partir d’un Enum mais l’important est que ça génère du code pour chaque élément qu’on voudra ajouter à un Enum.

On va maintenant passer au vif du sujet, le refactoring.

Refactoring

On fait le ménage

Supprimons ce qui ne va plus nous servir, à savoir : – La méthode description de l’Enum RoleEnum

Création de l’attribut

On va utiliser les attributs PHP, qui sont disponibles depuis la version 8. On va donc créer un attribut pour pouvoir récupérer les informations de l’Enum. Si vous n’êtes pas familiers avec le concept, voici la documentation : https://www.php.net/manual/en/language.attributes.overview.php.

Pour utiliser un attribut, il faut créer une classe correspondante. Créons, tout d’abord, une classe AttributeProperty qui sera la parente de tous nos attributs. On la place dans le dossier app/Attributes et on y ajoute le code suivant :

<?php

namespace App\Attributes;

use Attribute;

/* Indicate that Attribute will be use only on constants */#[Attribute(Attribute::TARGET_CLASS_CONSTANT)]
class AttributeProperty
{
    /* Will be useful to retrive attribute class later */    public const ATTRIBUTE_PATH = 'App\Attributes\\';

    public function __construct(
        private mixed $value,
    ) {
    }

    /**
     * Get the value of the attribute
     */    public function get(): mixed
    {
        return $this->value;
    }
}

Créons maintenant notre premier attribut, la description, pour remplacer notre exemple précédent. Pour ceci, on créer une classe Description dans le dossier app/Attributes et on y ajoute le code suivant :

<?php

namespace App\Attributes;

use Attribute;

#[Attribute(Attribute::TARGET_CLASS_CONSTANT)]
class Description extends AttributeProperty {}

Et ça suffit, notre classe d’attribut Description est déjà fonctionnelle. On va pouvoir l’utiliser dans notre Enum.

Rajoutons là comme ci-dessous :

<?php

namespace App\Enums;

use App\Attributes\Description;

enum RoleEnum: string
{
    #[Description('He\'s got full powers')]
    case ADMIN = 'admin';
    #[Description('A classic user, with classic rights')]
    case USER = 'user';
    #[Description('Oh, a guest, be nice to him!')]
    case GUEST = 'guest';
}

Création du trait AttributableEnum

Pour pouvoir utiliser un attribut il faut le récupérer. Pour cela, nous allons utiliser la Reflection API qui permet de récupérer des informations concernant les classes, les méthodes, les propriétés, etc. Pour plus d’informations, voici la documentation : https://www.php.net/manual/en/book.reflection.php.

L’idée est de pouvoir utiliser cette méthode sur tous nos futurs Enums. On va donc créer un trait AttributableEnum dans le dossier app/Traits et y ajouter le code suivant :

<?php

namespace App\Traits;

trait AttributableEnum
{
    /* ... */}

On ajoute ce trait à notre Enum app/Enums/RoleEnum.phpcomme ceci :

<?php

namespace App\Enums;

use App\Attributes\Description;
use App\Traits\AttributableEnum;

enum RoleEnum: string
{
    /* We use the trait we have created */    use AttributableEnum;

    #[Description('He\'s got full powers')]
    case ADMIN = 'admin';
    #[Description('A classic user, with classic rights')]
    case USER = 'user';
    #[Description('Oh, a guest, be nice to him!')]
    case GUEST = 'guest';
}

Notre objectif est d’appeler la description du rôle comme ceci dans la vue :

<x-layout>
   <h1 class="text-3xl font-bold">Users</h1>
    <ul>
        @foreach ($users as $user)
            <li class="flex justify-between px-4 py-2 my-4 bg-white border border-gray-300 rounded">
                <div>
                    <p class="font-semibold">{{ $user->name }}</p>
                    <p class=text-gray-500>{{ $user->email }}</p>
                </div>
                <div class="text-right">
                    <p class="font-bold text-gray-500">{{ $user->role }}</p>

                    <!-- Call the attribute description for this Enum case -->
                    <p class="text-gray-500">{{ $user->role->description() }}</p>
                    
                </div>
            </li>
        @endforeach
    </ul>
</x-layout>

Pour cela, on va modifier le trait app/Traits/AttributableEnum.php et y ajouter la méthode suivante :

<?php

namespace App\Traits;

use App\Attributes\AttributeProperty;
use BadMethodCallException;
use Illuminate\Support\Str;
use ReflectionAttribute;
use ReflectionEnum;

trait AttributableEnum
{
    /**
     * Call the given method on the enum case
     *
     */    public function __call(string $method, array $arguments): mixed
    {
        // Get attributes of the enum case with reflection API
        $reflection = new ReflectionEnum(static::class);
        $attributes = $reflection->getCase($this->name)->getAttributes();

        // Check if attribute exists in our attributes list
        $filtered_attributes = array_filter($attributes, fn (ReflectionAttribute $attribute) => $attribute->getName() === AttributeProperty::ATTRIBUTE_PATH . Str::ucfirst($method));

        // If not, throw an exception
        if (empty($filtered_attributes)) {
            throw new BadMethodCallException(sprintf('Call to undefined method %s::%s()', static::class, $method));
        }

        return array_shift($filtered_attributes)->newInstance()->get();
    }
}

Pour détailler le processus :

  • Tout d’abord la méthode magique __call() nous permet d’intercepter un appel à une méthode qui n’existe pas sur notre classe. Comme il n’existe pas de méthode description()sur notre Enum, on va pouvoir l’intercepter et faire ce que l’on veut avec.
  • La classe ReflectionEnum permet de récupérer toutes les informations de notre RoleEnum. On va pouvoir récupérer les attributs de notre Enum de la manière suivante : $reflection->getCase($this->name)->getAttributes(). $this->name correspond au nom du cas de l’Enum.
  • On va ensuite filtrer les attributs pour ne garder que ceux qui correspondent à notre attribut Description avec la méthode array_filter. On utilise la constante AttributeProperty::ATTRIBUTE_PATH pour récupérer le chemin complet de notre attribut Description qui est App\Attributes\Description.
  • Si on ne trouve pas d’attribut correspondant, on lance une exception BadMethodCallException pour indiquer que la méthode n’existe pas.
  • Si on trouve un attribut correspondant, on va l’instancier et appeler sa méthode get() pour récupérer la valeur qui nous intéresse.

Et voilà, avec la combinaison du Trait et des attributs PHP, on a donné des super pouvoirs à nos Enums.

Création de l’attribut BackgroundColor

On va maintenant créer un nouvel attribut pour gérer la couleur de fond de chaque rôle. Pour cela, on va ajouter une classe BackgroundColor dans le dossier app/Attributes et y ajouter le même code que pour l’attribut Description pour l’instant.

On va aller un peu plus loin en faisant en sorte de récupérer directement la classe Tailwind pour la couleur de fond.

<?php

namespace App\Attributes;

use Attribute;

#[Attribute(Attribute::TARGET_CLASS_CONSTANT)]
class BackgroundColor extends AttributeProperty
{

    public function __construct(
        private mixed $value,
    ) {
    }

    /**
     * Get the value of the attribute
     */    public function get(): string
    {
        return 'bg-' . $this->value . '-100';
    }
}

Et ça y est, on peut l’utiliser dans notre Enum app/Enums/RoleEnum.php comme ceci :

enum RoleEnum: string
{
    use AttributableEnum;

    #[Description('He\'s got full powers')]
    #[BackgroundColor('red')]
    case ADMIN = 'admin';

    #[Description('A classic user, with classic rights')]
    #[BackgroundColor('blue')]
    case USER = 'user';

    #[Description('Oh, a guest, be nice to him!')]
    #[BackgroundColor('green')]
    case GUEST = 'guest';
}

Et on peut l’utiliser dans notre vue resources/views/users.blade.php, en modifiant également le style de la page, comme ceci :

<x-layout>
   <h1 class="text-3xl font-bold">Users</h1>
    <ul>
        @foreach ($users as $user)
            <li class="flex justify-between px-4 py-2 my-4 bg-white border border-gray-300 rounded">
                <div class="flex flex-col justify-around">
                    <p class="text-lg font-semibold">{{ $user->name }}</p>
                    <p class=text-gray-500>{{ $user->email }}</p>
                </div>
                <!-- We add the class generated by the Attribute here -->
                <div class="text-right px-4 py-1 mx-1 my-2 rounded-md {{  $user->role->backgroundColor()  }}">
                    <p class="font-bold text-gray-800/50">{{ $user->role }}</p>

                    <!-- Call the attribute description for this Enum case -->
                    <p class="text-gray-500">{{ $user->role->description() }}</p>
                </div>
            </li>
        @endforeach
    </ul>
</x-layout>

Tailwind, par défaut, génère seulement les classes présentes dans le code. Comme nos classes sont générées dynamiquement, on va devoir rajouter une configuration pour que Tailwind génère les classes de background-color. Pour cela, on va modifier le fichier tailwind.config.js en y ajoutant une safelist comme ci-dessous :

/** @type {import('tailwindcss').Config} */export default {
  content: [
    "./resources/**/*.blade.php",
    "./resources/**/*.js",
    "./resources/**/*.vue",
  ],
  theme: {
    extend: {},
  },
  safelist: [
    {
      /* We want any bg color class to be generated */  
      pattern: /^bg-\w+-\d{2,3}$/,
    }
  ],
  plugins: [],
}

Et voilà le résultat:

Création de l’attribut Label

Pour finir, on voudrait que le nom du rôle soit affiché d’une manière plus jolie. On va donc créer un attribut Label pour ça. On va créer une classe Label dans le dossier app/Attributes et y ajouter le code suivant :

<?php

namespace App\Attributes;

use Attribute;

#[Attribute(Attribute::TARGET_CLASS_CONSTANT)]
class Label extends AttributeProperty {}

On l’applique à notre Enum app/Enums/RoleEnum.php comme ceci :

<?php

namespace App\Enums;

use App\Attributes\BackgroundColor;
use App\Attributes\Description;
use App\Attributes\Label;
use App\Traits\AttributableEnum;

enum RoleEnum: string
{
    use AttributableEnum;

    #[Label('Administrator')]
    #[Description('He\'s got full powers')]
    #[BackgroundColor('red')]
    case ADMIN = 'admin';

    #[Label('User')]
    #[Description('A classic user, with classic rights')]
    #[BackgroundColor('blue')]
    case USER = 'user';

    #[Label('Guest')]
    #[Description('Oh, a guest, be nice to him!')]
    #[BackgroundColor('green')]
    case GUEST = 'guest';
}

On peut, à présent, l’afficher dans la vue resources/views/users.blade.php comme ceci :

<x-layout>
   <h1 class="text-3xl font-bold">Users</h1>
    <ul>
        @foreach ($users as $user)
            <li class="flex justify-between px-4 py-2 my-4 bg-white border border-gray-300 rounded">
                <div class="flex flex-col justify-around">
                    <p class="text-lg font-semibold">{{ $user->name }}</p>
                    <p class=text-gray-500>{{ $user->email }}</p>
                </div>
                <!-- We add the class generated by the Attribute here -->
                <div class="text-right px-4 py-1 mx-1 my-2 rounded-md {{  $user->role->backgroundColor()  }}">
                    <!-- We use the value from Label attribute here -->
                    <p class="font-bold text-gray-800/50">{{ $user->role->label() }}</p>

                    <!-- Call the attribute description for this Enum case -->
                    <p class="text-gray-500">{{ $user->role->description() }}</p>
                </div>
            </li>
        @endforeach
    </ul>
</x-layout>

Et voilà la version finale :

Conclusion

On peut avoir besoin de greffer pas mal de choses à des Enums, pour ma part, mon besoin était sur un choix de prestation, pour les valeurs d’un select dans un formulaire de réservation. J’avais besoin d’éléments de contenu formatés, d’informations complémentaires, et d’éléments utiles aux règles de validation. Cette approche a simplifié mon code et a apporté une grande flexibilité dans le développement.

Si vous ne voulez pas le faire vous-même, mais que vous souhaitez utiliser un package, je vous conseille d’aller voir celui-ci, il m’a donné l’idée de creuser ce sujet et propose des ajouts intéressants sur les Enums: https://github.com/archtechx/enums

Pour finir, si vous vous posez des questions sur des sujets de dev, n’hésitez pas à creuser, on apprend beaucoup en le faisant.

 

À propos de moi :

Développeur Fullstack depuis 3 ans suite à une reconversion professionnelle après 5 ans de travail dans le Webmarketing.
J’ai fait ma formation de développeur chez Oclock Je travaille en CDI dans une entreprise spécialisée en marketing / gestion de projet B2B. Mon quotidien professionnel c’est de développer sur une plateforme d’e-commerce et un ERP maison qui couvre tous les métiers de notre entreprise. Je travaille sur un framework maison, sur Laravel et sur VueJs. Et côté perso je suis passionné par le web plus créatif, je travaille beaucoup avec VueJs, ThreeJs, Gsap pour faire des projets fun.
Retrouver moi sur X

Damien Toscano

Recent Posts

MICI au travail : Le handicap invisible qui révèle des forces insoupçonnées

Les maladies inflammatoires chroniques de l’intestin ou "MICI" sont invisibles, mais leurs impacts sur la…

3 jours ago

Exploiter les NPUs pour de l’IA embarquée dans les applis webs

Depuis l'été, j'ai un Pixel qui intègre à la fois un TPU (Tensor Processing Unit)…

1 semaine ago

Qcm saison hiver 2024 : toutes les infos.

On se retrouve dans un nouvel article avec toutes les infos sur cette nouvelle saison…

3 semaines ago

L’inclusion numérique est essentielle.

Pourquoi l’inclusion numérique est essentielle : le point avec Mathieu Froidure. Dans un monde de…

4 semaines ago

Communauté Tech et féminine : Interview avec Helvira de Motiv’her

Elles sont passées où les femmes dans la tech ? Entre le manque de représentation…

1 mois ago