Skip to Content

Chapitre 2.3 — Les enums

Partie 2 : PHP moderne — Chapitre 3/8 Version de référence : PHP 8.1+ (enums) pour Symfony 7.4.

⏱️ TL;DR

  • Une enum (PHP 8.1) est un type dont les valeurs sont un ensemble fini de cas nommés : BookingStatus::Confirmed. Fini les constantes string bricolées.
  • Deux formes : pure (enum Suit { case Hearts; … }) et « backed » (adossée à une valeur string ou int : enum Status: string { case Confirmed = 'confirmed'; }).
  • Méthodes utiles : Status::cases() (tous les cas), Status::from('confirmed') (lève si inconnu), Status::tryFrom('x') (renvoie null si inconnu).
  • Une enum peut avoir des méthodes, des constantes, et implémenter des interfaces — bien plus puissant qu’une union de littéraux TS.
  • Un cas d’enum est un objet singleton : Status::Confirmed === Status::Confirmed est toujours vrai ; on compare avec ===.
  • Dans Symfony : statuts, rôles, mappés directement en base par Doctrine (enumType), utilisés dans les formulaires, les match, l’API.

🎯 Objectifs

  • Déclarer des enums pures et backed, et savoir laquelle choisir.
  • Utiliser cases(), from(), tryFrom() à bon escient.
  • Ajouter méthodes, constantes et interfaces à une enum.
  • Comprendre comment Symfony/Doctrine exploitent les enums (aperçu, détaillé plus tard).

1. Le problème que les enums résolvent

Avant PHP 8.1, on représentait un statut par des constantes de chaîne :

class Booking { const STATUS_PENDING = 'pending'; const STATUS_CONFIRMED = 'confirmed'; const STATUS_CANCELLED = 'cancelled'; private string $status; // ← n'importe quelle string passe : 'confrimed', 'PENDING', 'foo'… }

Le type de $status est string : rien n’empêche d’y mettre 'confrimed' (faute de frappe) ou 'foo'. Les enums règlent ça : le type devient BookingStatus, et seuls les cas déclarés existent.

declare(strict_types=1); enum BookingStatus: string { case Pending = 'pending'; case Confirmed = 'confirmed'; case Cancelled = 'cancelled'; } // $booking->status est de type BookingStatus : impossible d'y mettre une string invalide.

💡 Pour un dev Next.js : c’est l’équivalent d’une union de littéraux TS (type Status = "pending" | "confirmed" | "cancelled") — mais réifiée en objet : on peut lui attacher des méthodes, itérer sur ses cas, la mapper en base. C’est le meilleur des deux mondes entre l’enum TS et l’union de littéraux.

2. Enums pures vs « backed »

2.1 Enum pure

Pas de valeur associée : les cas ne sont que des identités.

enum Direction { case North; case South; case East; case West; } $d = Direction::North;

2.2 Enum « backed » (adossée à une valeur)

Chaque cas a une valeur scalaire (string ou int), obligatoire dès qu’on veut persister ou sérialiser (base de données, JSON, formulaire) :

enum BookingStatus: string // ← le « : string » en fait une backed enum { case Pending = 'pending'; case Confirmed = 'confirmed'; case Cancelled = 'cancelled'; } echo BookingStatus::Confirmed->value; // 'confirmed' (accès à la valeur via ->value)

Règle pratique : dès qu’une enum touche la base de données, une API ou un formulaire, prenez une backed enum (string de préférence, plus lisible en base et dans les payloads JSON qu’un int).

⚠️ Piège : la propriété magique ->value n’existe que sur les backed enums. Sur une enum pure, Direction::North->value provoque une erreur (il n’y a pas de valeur). À l’inverse, ->name existe sur les deux (BookingStatus::Confirmed->name vaut la chaîne 'Confirmed', le nom du cas).

3. Les trois méthodes à connaître

Toute enum backed dispose de méthodes statiques bien commodes :

// 1) Tous les cas (dans l'ordre de déclaration) → tableau d'instances BookingStatus::cases(); // [BookingStatus::Pending, BookingStatus::Confirmed, BookingStatus::Cancelled] // 2) from() : convertit une valeur → cas, ou LÈVE une ValueError si inconnue BookingStatus::from('confirmed'); // BookingStatus::Confirmed BookingStatus::from('foo'); // 💥 ValueError // 3) tryFrom() : idem mais renvoie null si inconnue (pas d'exception) BookingStatus::tryFrom('confirmed'); // BookingStatus::Confirmed BookingStatus::tryFrom('foo'); // null

Usage typique côté API : on reçoit une string du client et on la convertit prudemment.

$status = BookingStatus::tryFrom($payload['status'] ?? '') ?? throw new \InvalidArgumentException('Statut invalide');

💡 Pour un dev Next.js : cases()Object.values(Status), mais typé et ordonné. tryFrom() ≈ un parseur sûr (façon zod .safeParse) qui renvoie null plutôt que de lever. from() ≈ la version qui throw. Ces trois méthodes remplacent tout le code de validation que vous écririez à la main.

4. Une enum peut avoir des méthodes (là où TS ne peut pas)

C’est la vraie supériorité des enums PHP : ce sont des objets, donc elles portent du comportement.

enum BookingStatus: string { case Pending = 'pending'; case Confirmed = 'confirmed'; case Cancelled = 'cancelled'; // Une méthode « d'instance » : $status->label() public function label(): string { return match ($this) { self::Pending => 'En attente', self::Confirmed => 'Confirmée', self::Cancelled => 'Annulée', }; } // Une méthode métier public function isActive(): bool { return $this !== self::Cancelled; } // Une constante d'enum public const string DEFAULT = self::Pending->value; } BookingStatus::Confirmed->label(); // 'Confirmée' BookingStatus::Cancelled->isActive(); // false

On regroupe ainsi la donnée et son comportement : le libellé français, la logique « est-ce actif ? »… tout vit dans l’enum, au lieu d’être éparpillé dans des if partout. Le match sur $this est exhaustif : si vous ajoutez un cas sans traiter le match, PHP lève une erreur à l’exécution (et PHPStan la signale statiquement).

5. Enums et interfaces

Une enum peut implémenter une interface, ce qui permet de la traiter de façon polymorphe :

interface HasColor { public function color(): string; } enum BookingStatus: string implements HasColor { case Pending = 'pending'; case Confirmed = 'confirmed'; case Cancelled = 'cancelled'; public function color(): string { return match ($this) { self::Pending => 'orange', self::Confirmed => 'green', self::Cancelled => 'red', }; } }

Il existe aussi l’interface native \UnitEnum (toute enum) et \BackedEnum (enums backed) — utiles pour typer « n’importe quelle enum » dans du code générique.

⚠️ Piège : une enum ne peut pas avoir d’état d’instance (pas de propriétés modifiables) — ses cas sont des singletons immuables. Vous ne pouvez pas faire $status->count = 3. Si vous avez besoin d’attacher des données mutables, ce n’est pas une enum qu’il vous faut, mais un objet classique.

6. Les enums dans Symfony (aperçu)

Vous croiserez les enums partout dans le cours ; retenez les usages, on détaillera plus tard :

  • Doctrine (Partie 6) mappe une enum directement sur une colonne :
    #[ORM\Column(enumType: BookingStatus::class)] private BookingStatus $status = BookingStatus::Pending;
    En base, la colonne stocke la valeur ('confirmed') ; en PHP, vous manipulez l’enum.
  • Formulaires (Partie 9) : EnumType génère une liste déroulante à partir des cas.
  • API Platform (Partie 11) : une enum backed se sérialise naturellement en sa valeur dans le JSON.
  • Rôles/permissions : on modélise souvent les rôles ou des permissions fines par une enum.

📚 Aller plus loin : la RFC et la doc officielle des enums (php.net/enumerations ) détaillent les subtilités (constantes, cases(), interfaces). C’est une lecture rapide et rentable.

✏️ Exercices

Exercice 1. Créez une enum backed Role: string avec Admin, Provider, Customer, une méthode label(): string (libellés français) et une méthode canManageBookings(): bool (vrai pour Admin et Provider).

✅ Solution

declare(strict_types=1); enum Role: string { case Admin = 'ROLE_ADMIN'; case Provider = 'ROLE_PROVIDER'; case Customer = 'ROLE_CUSTOMER'; public function label(): string { return match ($this) { self::Admin => 'Administrateur', self::Provider => 'Prestataire', self::Customer => 'Client', }; } public function canManageBookings(): bool { return match ($this) { self::Admin, self::Provider => true, self::Customer => false, }; } }

(On a aligné les valeurs sur la convention Symfony ROLE_*, utile en Partie 10.)

Exercice 2. Depuis une requête API, vous recevez $data['role'] (une string éventuellement invalide). Convertissez-la en Role, en renvoyant une erreur claire si elle est inconnue.

✅ Solution

$role = Role::tryFrom($data['role'] ?? '') ?? throw new \InvalidArgumentException( sprintf('Rôle inconnu : "%s"', $data['role'] ?? '') );

tryFrom() renvoie null si la valeur n’est pas un cas ; l’opérateur ?? déclenche alors le throw (une expression depuis PHP 8.0). On évite ainsi le ValueError brut de from() au profit d’un message métier.

Exercice 3. Pourquoi BookingStatus::Confirmed === BookingStatus::Confirmed est-il toujours true, et que vaut BookingStatus::Confirmed->name ?

✅ Solution

Chaque cas d’enum est un singleton : il n’existe qu’une seule instance de BookingStatus::Confirmed en mémoire, donc la comparaison d’identité === est toujours vraie. BookingStatus::Confirmed->name vaut la chaîne 'Confirmed' (le nom du cas) ; sa ->value vaudrait 'confirmed' (la valeur backed).

🧠 Quiz de révision

1. Différence entre une enum pure et une enum backed ?

Une enum pure n’a que des cas nommés (identités). Une enum backed (enum X: string|int) associe à chaque cas une valeur scalaire, nécessaire pour persister/sérialiser (base, JSON, formulaire).

2. Que font from() et tryFrom() ?

Elles convertissent une valeur en cas d’enum. from() lève un ValueError si la valeur est inconnue ; tryFrom() renvoie null dans ce cas.

3. En quoi une enum PHP est-elle plus puissante qu’une union de littéraux TS ?

Elle est réifiée en objet : elle peut porter des méthodes, des constantes, implémenter des interfaces, et on peut itérer sur ses cas via cases().

4. Peut-on stocker un état mutable dans un cas d’enum ?

Non : les cas sont des singletons immuables, sans propriétés modifiables. Pour de l’état mutable, il faut une classe classique.

5. Comment Doctrine mappe-t-il une enum sur une colonne ?

Via #[ORM\Column(enumType: BookingStatus::class)] : la base stocke la valeur (ex. 'confirmed'), et Doctrine reconstruit l’enum côté PHP.


Prochain chapitre : 2.4 — Les attributs — la métadonnée native #[...] qui remplace les annotations, et comment Symfony la lit.