Un tunnel applicatif a 7 étapes. Au départ, l’information vivait dans le contrôleur sous forme d’arrays de constantes :
final class TunnelController
{
private const STEPS = [
1 => 'Coordonnées',
2 => 'Bénéficiaires',
3 => 'Votre contrat',
4 => 'Récapitulatif',
5 => 'Pièces justificatives',
6 => 'Signature',
7 => 'Paiement',
];
private const STEP_SLUGS = [
1 => 'coordonnees',
2 => 'beneficiaires',
// … le même mapping, en double, ailleurs
];
}
Ça « marche ». Et ça pose exactement les problèmes qu’un enum PHP 8.1 résout d’un coup.
Ce qui cloche avec l’array de constantes
- Pas de type. Une fonction reçoit
int $step. Rien n’empêcherenderStep(99)— l’array renvoienull, et on découvre le bug à l’exécution. - La connaissance est dupliquée. Le libellé est dans un array, le slug dans un autre, la résolution
slug → numéroréimplémentée à la main quelque part. Trois sources pour une seule notion « étape ». - Pas de complétude vérifiée. Ajouter une 8ᵉ étape demande de penser à mettre à jour tous les arrays. Le compilateur ne vous aide pas.
- Les valeurs traînent dans tout le code.
if ($step === 5)— le fameux magic number. Que vaut 5 ? Il faut aller relire la constante.
L’enum backed, avec ses comportements attachés
Un enum PHP 8.1 backed (adossé à un int) regroupe la valeur, ses libellés et sa logique de résolution au même endroit :
enum Step: int
{
case Coordonnees = 1;
case Beneficiaires = 2;
case VotreContrat = 3;
case Recapitulatif = 4;
case PiecesJustificatives = 5;
case Signature = 6;
case Paiement = 7;
public function slug(): string
{
return match ($this) {
self::Coordonnees => 'coordonnees',
self::Beneficiaires => 'beneficiaires',
self::VotreContrat => 'votre-contrat',
self::Recapitulatif => 'recapitulatif',
self::PiecesJustificatives => 'pieces-justificatives',
self::Signature => 'signature',
self::Paiement => 'paiement',
};
}
public function title(): string
{
return match ($this) {
self::Coordonnees => 'Coordonnées',
self::Beneficiaires => 'Bénéficiaires',
self::VotreContrat => 'Votre contrat',
self::Recapitulatif => 'Récapitulatif',
self::PiecesJustificatives => 'Pièces justificatives',
self::Signature => 'Signature',
self::Paiement => 'Paiement',
};
}
public static function fromSlug(string $slug): ?self
{
foreach (self::cases() as $step) {
if ($step->slug() === $slug) {
return $step;
}
}
return null;
}
}
Le gain n’est pas cosmétique :
- Le
matchest exhaustif. Si j’ajoutecase Confirmation = 8;sans compléterslug(), PHP lève uneUnhandledMatchError— unmatchsansdefaultforce à traiter le nouveau cas. L’array, lui, aurait silencieusement renvoyénull. Le compilateur devient mon filet. - Le type remplace le magic number.
if ($step === Step::PiecesJustificatives)se lit tout seul, et une signaturerender(Step $step)rend impossible de passer un entier hors domaine. - Une seule source de vérité. Valeur, slug, titre, résolution inverse : tout est dans l’enum. Plus de mapping dupliqué à garder synchronisé.
cases()énumère pour moi. Construire la barre de progression, c’estforeach (Step::cases() as $step)— l’ordre et la complétude sont garantis par la déclaration.
Côté routing, la résolution slug → étape (et son rejet propre des slugs inconnus) tient dans fromSlug() :
$step = Step::fromSlug($request->attributes->get('slug'));
if ($step === null) {
throw $this->createNotFoundException();
}
$this->render($step->title(), $step->slug());
Le piège à éviter : tout fourrer dans l’enum
Un enum n’est pas une fourre-tout. Ici, je n’ai pas modélisé la 8ᵉ « étape » de confirmation finale dedans : elle n’a ni formulaire ni état de session, elle est exposée par une route distincte. La forcer dans l’enum aurait obligé à des cas spéciaux (slug() qui renvoie quoi ? title() ?) pour une chose qui n’est pas vraiment une étape du même genre. L’enum modélise les 7 étapes homogènes ; l’exception reste dehors. Un enum gagne sa valeur à rester un ensemble cohérent.
Lessons learned
- Un array de constantes
int → stringest presque toujours un enum qui s’ignore. Dès qu’il y a une notion fermée (étapes, statuts, types) avec des libellés attachés, l’enum backed PHP 8.1 la modélise mieux : type fort, source unique, comportements méthodes. - Le
matchsansdefaultest un garde de complétude gratuit. Ajouter un cas sans traiter ses méthodes lève une erreur à l’exécution immédiate — là où un array renvoienullen silence. Évitez ledefaultjustement pour conserver ce filet. - Attachez le comportement à l’enum (
slug(),title(),fromSlug()) plutôt que de réimplémenter le mapping ailleurs. La connaissance « ce qu’est une étape » vit à un seul endroit. Enum::cases()remplace les boucles sur index. Itérer les étapes pour une stepbar devient trivial et garanti complet.- Ne forcez pas les exceptions dans l’enum. Un élément qui n’a pas la même nature que les autres (ici la page de confirmation) reste en dehors. Un enum garde sa valeur tant qu’il décrit un ensemble homogène.
#php #symfony #enum #refactoring #kiss