← Tous les cours / Design Patterns PHP Cours
00 — POURQUOI LES PATTERNS

Les design patterns : un vocabulaire, pas une checklist

Un design pattern n'est pas une brique à empiler pour faire « pro ». C'est un nom donné à une solution que tu réinventes déjà sans le savoir — et pouvoir la nommer change tout dans une équipe.

Imagine deux menuisiers. L'un dit « tu sais, l'assemblage où on découpe deux pièces en zigzag pour qu'elles s'emboîtent solidement en angle » ; l'autre dit « une queue d'aronde ». Même solution, mais le second a un mot. Ce mot condense des décennies d'expérience, se transmet en une seconde, et évite les malentendus. Les design patterns sont les queues d'aronde du développeur.

Design pattern
Une solution éprouvée à un problème de conception qui revient souvent, décrite de façon assez générale pour être réutilisée dans des contextes variés. Ce n'est pas du code prêt à copier, mais un modèle de solution — et surtout un nom partagé qui fait gagner un temps fou en discussion.

La référence historique, c'est le livre Design Patterns de 1994, signé par quatre auteurs qu'on surnomme le « Gang of Four ». Ils y cataloguent 23 patterns, rangés en trois familles selon le problème qu'ils résolvent :

FamilleRépond àExemples
CréationnelsComment créer des objets ?Factory, Builder, Singleton
StructurelsComment assembler des objets ?Decorator, Adapter, Composite
ComportementauxComment les objets collaborent ?Strategy, Observer, Command

Ce cours ne va pas les survoler tous les 23 — ce serait un annuaire. On va d'abord poser les principes SOLID, la boussole qui dit pourquoi un design est bon, puis étudier en profondeur cinq patterns que tu croiseras vraiment en PHP, avec des simulateurs pour les sentir agir. On finira par une révélation : Symfony lui-même est un empilement de ces patterns.

🚨 Le piège numéro un, dit tout de suite : les patterns ne sont pas un but. Coller un pattern « parce que c'est propre » sur un problème qui n'en a pas besoin produit un code plus compliqué, pas plus clair. Un pattern se mérite par un problème réel. On y consacre une section entière (09) — garde ce doute salutaire tout du long.
01 — LES PRINCIPES SOLID

SOLID : la boussole avant les patterns

Avant de connaître les solutions toutes faites, il faut savoir reconnaître un bon design d'un mauvais. Cinq principes, un acronyme, et une même obsession : que le changement ne fasse pas tout s'écrouler.

Les patterns sont des solutions ; SOLID dit pourquoi elles sont bonnes. Ce sont cinq principes de conception orientée objet, popularisés par Robert C. Martin sous cet acronyme. Leur fil rouge commun : rendre le code ouvert au changement sans être fragile. Prenons-les un par un, avec le compte que tu voudras.

S — Single Responsibility
Une classe ne devrait avoir qu'une seule raison de changer. Si ta classe Facture calcule des montants et génère du PDF et envoie des e-mails, trois raisons différentes la feront évoluer — et chaque changement risque de casser les deux autres responsabilités. Sépare-les.
O — Open/Closed
Une entité doit être ouverte à l'extension, fermée à la modification. Ajouter un nouveau mode de livraison ne devrait pas t'obliger à rouvrir et modifier la classe existante (au risque de casser l'existant), mais à ajouter une nouvelle classe. C'est le principe que le pattern Strategy incarne le mieux.
L — Liskov Substitution
Un sous-type doit pouvoir remplacer son type parent sans casser le programme. Si Carre hérite de Rectangle mais que fixer sa largeur change aussi sa hauteur, tu violes le contrat attendu d'un rectangle — et tout code qui manipule des rectangles se met à mentir.
I — Interface Segregation
Mieux vaut plusieurs interfaces spécifiques qu'une seule interface fourre-tout. Une classe ne devrait pas être forcée d'implémenter des méthodes dont elle n'a que faire. Une grosse interface Machine avec imprimer(), scanner(), faxer() force une imprimante simple à faire semblant de faxer.
D — Dependency Inversion
Les modules de haut niveau ne devraient pas dépendre des détails, mais d'abstractions. Ta logique métier dépend d'une interface EnvoiEmail, pas de la classe concrète SmtpMailer. On l'a vu en pratique dans le cours repositories & factories : le métier définit le contrat, l'infrastructure le réalise.
🔑 SOLID n'est pas une loi, c'est une tension à équilibrer. Appliqué à l'aveugle, il produit une prolifération de micro-classes et d'interfaces qui noie le sens. Vu comme un jeu de questions — « combien de raisons de changer ? puis-je étendre sans modifier ? cette abstraction sert-elle vraiment ? » — il aiguise le jugement. Les patterns qui suivent sont, pour la plupart, des façons concrètes d'honorer l'un de ces principes.
02 — STRATEGY

Strategy : changer d'algorithme sans toucher au reste

Tu as plusieurs façons de faire une même chose — calculer des frais de port, trier, appliquer une remise — et tu veux pouvoir passer de l'une à l'autre sans un buisson de if. Le pattern Strategy encapsule chaque façon dans son propre objet.

Le symptôme qui appelle Strategy, c'est le if/elseif qui gonfle. « Si livraison standard, alors… sinon si express… sinon si point relais… ». Chaque nouveau mode de livraison rouvre la même méthode, rallonge la chaîne, et menace de casser les cas existants — une violation frontale du principe Open/Closed.

Strategy
Encapsuler chaque variante d'un algorithme dans une classe distincte, derrière une interface commune, et rendre ces variantes interchangeables. Le code client choisit une stratégie et l'utilise sans connaître son détail. Ajouter une variante = ajouter une classe, sans rien modifier.
interface FraisDePort
{
    public function calculer(int $totalPanier): int;   // en centimes
}

final class Express implements FraisDePort
{
    public function calculer(int $totalPanier): int { return 1200; }
}

final class OffertDesUnSeuil implements FraisDePort
{
    public function __construct(private int $seuil = 5000) {}
    public function calculer(int $totalPanier): int
    {
        return $totalPanier >= $this->seuil ? 0 : 600;   // gratuit au-dessus du seuil
    }
}

// Le client reçoit UNE stratégie et l'utilise sans savoir laquelle.
final class Commande
{
    public function __construct(private FraisDePort $fraisDePort) {}
    public function total(int $panier): int
    {
        return $panier + $this->fraisDePort->calculer($panier);
    }
}

Remarque comme Commande ne contient plus aucun if sur le mode de livraison : elle délègue à la stratégie qu'on lui a confiée. Le simulateur ci-dessous te laisse permuter la stratégie sur un même panier — et l'une d'elles change de comportement selon le montant, ce qu'aucune autre ne fait.

Un même panier, plusieurs stratégies de frais de port. Change de montant, puis permute la stratégie. Observe comme « offert dès 50 € » réagit au panier là où les autres restent fixes — c'est la variabilité encapsulée.

Panier :
Frais de port :
Total :
💡 Où tu l'utilises déjà : les voters de Symfony Security sont des stratégies d'autorisation ; les différents encodeurs de mot de passe, les stratégies de sérialisation… Chaque fois qu'un composant te laisse « brancher » un comportement via une interface, c'est un Strategy qui sommeille.
03 — FACTORY

Factory : déléguer la création

Faire un new au milieu de ta logique métier, c'est se marier avec une classe concrète pour toujours. La Factory déporte cette décision ailleurs, pour que ton code parle d'abstractions.

On a déjà rencontré la factory dans le cours repositories & factories, côté DDD. Reprenons-la ici comme pattern de conception général, car elle répond à un problème précis : un new MaClasseConcrete() écrit en plein cœur de ta logique la couple définitivement à cette classe. Impossible d'en changer sans rouvrir ce code.

Factory
Déplacer la responsabilité de créer des objets vers un objet ou une méthode dédiée. Le code client demande « fabrique-moi le bon objet » sans connaître la classe concrète produite. Utile quand la création dépend d'une condition, cache un type concret, ou constitue une opération à part entière.

Un exemple parlant : choisir un moyen de paiement selon un code, sans que le code appelant ne connaisse les classes concrètes.

final class PaiementFactory
{
    public function creer(string $type): MoyenPaiement
    {
        return match ($type) {
            'cb'      => new CarteBancaire(),
            'paypal'  => new PayPal(),
            'virement' => new Virement(),
            default   => throw new MoyenInconnuException($type),
        };
    }
}

// Le client ne connaît que l'abstraction MoyenPaiement.
$paiement = $factory->creer($choixUtilisateur);
$paiement->payer($montant);

Le Gang of Four distingue plusieurs variantes — la Factory Method (une méthode que les sous-classes redéfinissent pour choisir le type produit) et l'Abstract Factory (une factory qui produit des familles d'objets cohérents). En pratique PHP, on rencontre surtout la « factory simple » ci-dessus, moins canonique mais très répandue.

💡 Où tu l'utilises déjà : le conteneur de services de Symfony est une immense factory — il fabrique tes services à la demande, résout leurs dépendances, cache les classes concrètes derrière des interfaces. Tu peux d'ailleurs déclarer explicitement une factory de service quand la construction est trop complexe pour l'autowiring.

La Factory est le complément naturel du principe de Dependency Inversion : elle est l'endroit — souvent le seul — où l'on assume de nommer une classe concrète, pour que tout le reste du code n'ait à connaître que des abstractions.

04 — DECORATOR

Decorator : empiler des comportements

Plutôt que d'ajouter dix options par héritage (et de finir avec une explosion de sous-classes), tu emballes un objet dans un autre qui lui ajoute une capacité. Et tu peux emballer l'emballage.

Suppose que tu veuilles ajouter des comportements optionnels à un traitement : mettre en forme un texte, l'horodater, l'encadrer en HTML. Par héritage, chaque combinaison exigerait sa propre sous-classe — TexteHorodateEtEncadre, TexteMajusculeEtHorodate… une explosion combinatoire. Le Decorator résout ça en emballant.

Decorator
Envelopper un objet dans un autre qui implémente la même interface et ajoute un comportement avant ou après avoir délégué à l'objet enveloppé. Comme le décorateur a la même interface que ce qu'il décore, on peut l'empiler à l'infini — un décorateur qui décore un décorateur.
interface Texte { public function rendu(): string; }

final class TexteBrut implements Texte
{
    public function __construct(private string $valeur) {}
    public function rendu(): string { return $this->valeur; }
}

// Un décorateur : même interface, enveloppe un Texte, ajoute sa touche.
final class Majuscules implements Texte
{
    public function __construct(private Texte $inner) {}
    public function rendu(): string { return strtoupper($this->inner->rendu()); }
}

final class Encadre implements Texte
{
    public function __construct(private Texte $inner) {}
    public function rendu(): string { return '[ ' . $this->inner->rendu() . ' ]'; }
}

// On empile : chaque couche enveloppe la précédente.
$t = new Encadre(new Majuscules(new TexteBrut('bonjour')));
$t->rendu();   // "[ BONJOUR ]"

Le simulateur te laisse activer les décorateurs un à un et voir la sortie se composer. Chaque couche transforme le résultat de la précédente — l'ordre d'empilement compte, exactement comme dans le code.

Un texte brut, des décorateurs à empiler. Active-les dans l'ordre que tu veux et regarde la sortie se transformer couche par couche. Chaque décorateur enveloppe le résultat du précédent.

texte brut : " bonjour le monde "
pile de décorateurs (de l'intérieur vers l'extérieur) :
sortie :
💡 Où tu l'utilises déjà : Symfony permet de décorer un service (attribut #[AsDecorator]) — remplacer un service par un autre qui l'enveloppe. Et le HttpCache est un décorateur du noyau HTTP : il implémente la même interface que le kernel, l'enveloppe, et ajoute la gestion du cache autour.
05 — OBSERVER

Observer : prévenir sans se connaître

Un objet change d'état, et d'autres doivent réagir — sans qu'il ait à les connaître un par un. L'Observer inverse le lien : ce sont les intéressés qui s'abonnent.

Une commande est payée. Il faut envoyer un e-mail de confirmation, mettre à jour le stock, notifier la comptabilité, déclencher la préparation. Si la classe Commande appelle elle-même ces quatre services, elle les connaît tous, dépend d'eux tous, et grossit à chaque nouveau besoin. C'est fragile et fermé au changement.

Observer
Un objet (le sujet) tient une liste d'observateurs intéressés par ses changements et les prévient quand quelque chose se produit — sans connaître leur nature. Les observateurs s'abonnent ; le sujet diffuse. Le couplage passe de « un vers plusieurs objets concrets » à « un vers une interface ».
interface Observateur { public function surCommandePayee(Commande $c): void; }

final class Commande
{
    private array $observateurs = [];

    public function abonner(Observateur $o): void { $this->observateurs[] = $o; }

    public function payer(): void
    {
        // … logique de paiement …
        foreach ($this->observateurs as $o) {
            $o->surCommandePayee($this);   // on prévient, on ne fait pas à leur place
        }
    }
}

Ajouter un nouveau réagissant — un programme de fidélité, par exemple — ne touche plus à Commande : on écrit un nouvel observateur et on l'abonne. La commande reste close à la modification, ouverte à l'extension.

💡 Où tu l'utilises déjà : tout l'EventDispatcher de Symfony est un Observer (avec une touche de Mediator) — on émet un événement, les listeners abonnés réagissent. Et les Domain Events du DDD sont la version métier de ce pattern : l'agrégat annonce un fait, d'autres parties du système y réagissent sans qu'il les connaisse.
🔑 La nuance avec Command : un événement (Observer) dit « ceci s'est produit » à qui veut l'entendre — plusieurs réactions possibles, ou aucune. Une commande (section suivante) dit « fais ceci » à un destinataire précis — une intention adressée. Le premier diffuse un fait passé ; le second demande une action. On les confond souvent ; garder cette distinction en tête clarifie énormément d'architectures.
06 — COMMAND

Command : transformer une action en objet

Emballer « ce qu'il faut faire » dans un objet ouvre des possibilités qu'un simple appel de méthode n'a pas : le mettre en file, le journaliser, le rejouer — et surtout, l'annuler.

Un appel de méthode, $editeur->ajouter('café'), s'exécute et disparaît. Impossible de le mettre de côté, de le passer à un worker, ou de l'annuler après coup. Le pattern Command transforme cette action en objet de première classe — et c'est ce qui débloque l'annulation.

Command
Encapsuler une requête (une action à effectuer, avec ses paramètres) dans un objet. Cet objet expose une méthode pour l'exécuter, et souvent une pour l'annuler. Devenue objet, l'action peut être stockée dans une file, journalisée, rejouée, ou empilée pour permettre un undo.
interface Commande
{
    public function executer(): void;
    public function annuler(): void;   // la clé de l'undo
}

final class AjouterArticle implements Commande
{
    public function __construct(private Panier $panier, private string $article) {}
    public function executer(): void { $this->panier->ajouter($this->article); }
    public function annuler(): void  { $this->panier->retirer($this->article); }
}

Un « invocateur » garde une pile des commandes exécutées ; annuler, c'est dépiler la dernière et appeler son annuler(). Le simulateur ci-dessous met ça en scène : ajoute des articles, puis annule et refais. Chaque action est une commande empilée.

Chaque action sur le panier est une commande empilée. Ajoute des articles, puis annule pas à pas et refais. L'historique des commandes rend l'undo/redo possible — c'est tout l'intérêt du pattern.

panier :
pile annulables : 0
pile refaisables : 0
💡 Où tu l'utilises déjà : un command bus comme Symfony Messenger est ce pattern à l'échelle de l'application — un message (la commande) est dispatché vers son handler, et peut être mis en file pour un traitement asynchrone. C'est aussi le « C » de CQRS : la commande y est l'intention d'écriture, distincte de la requête de lecture.
07 — ADAPTER

Adapter : faire parler deux interfaces incompatibles

Tu as un code qui attend une prise européenne et un appareil à fiche américaine. Tu ne réécris ni l'un ni l'autre : tu intercales un adaptateur. En logiciel, c'est exactement pareil.

Ton code métier attend une interface Logger avec une méthode log(string $message). La bibliothèque tierce que tu veux utiliser, elle, expose writeLine(array $data). Les deux sont incompatibles, et tu ne peux modifier ni ton métier ni la bibliothèque. L'Adapter est la pièce qui les réconcilie.

Adapter
Une classe qui implémente l'interface attendue par ton code et, en interne, traduit chaque appel vers l'interface réelle d'un objet incompatible. Ton code parle sa langue habituelle ; l'adaptateur fait la traduction, des deux côtés si besoin.
// Ce que ton code attend.
interface Logger { public function log(string $message): void; }

// L'adaptateur : implémente Logger, traduit vers la biblio tierce.
final class BiblioTierceAdapter implements Logger
{
    public function __construct(private BiblioTierce $biblio) {}

    public function log(string $message): void
    {
        // traduction de MON interface vers la SIENNE
        $this->biblio->writeLine(['msg' => $message, 'ts' => time()]);
    }
}

Le résultat : ton métier ne connaît que Logger, et rien de la bibliothèque tierce. Le jour où tu changes de bibliothèque, tu écris un nouvel adaptateur — le métier ne bouge pas d'un octet.

💡 Le pont avec le stratégique : l'Adapter est la brique tactique de l'Anti-Corruption Layer vu dans le cours DDD stratégique. Là où l'ACL protège tout un bounded context du modèle d'un système externe, l'Adapter fait la traduction à l'échelle d'une classe. Même idée — traduire pour ne pas se laisser contaminer —, deux échelles.
🔑 Adapter vs Decorator : les deux enveloppent un objet, mais dans des buts opposés. Le Decorator garde la même interface et ajoute un comportement. L'Adapter change l'interface sans rien ajouter de fonctionnel : il traduit. Reconnaître lequel tu écris t'évite bien des confusions.
08 — LES PATTERNS DANS SYMFONY

Tu fais déjà des patterns : Symfony en est tissé

Le meilleur moyen de comprendre un pattern, c'est de le reconnaître dans un outil que tu utilises tous les jours. Symfony est un catalogue vivant du Gang of Four.

Une fois qu'on a le vocabulaire, on ne peut plus le désapprendre : les patterns sont partout dans les frameworks que tu utilises. Symfony en particulier est un empilement lisible de ces solutions. Faire le tour, c'est ancrer chaque pattern dans du concret que tu manipules déjà.

PatternDans Symfony
StrategyLes voters de sécurité, les encodeurs de mot de passe, les normalizers du Serializer — des comportements interchangeables derrière une interface.
ObserverL'EventDispatcher tout entier : on émet, les listeners abonnés réagissent.
CommandLe bus de Messenger : un message dispatché vers son handler, éventuellement mis en file.
FactoryLe conteneur de services, et les factories de service explicites.
DecoratorLa décoration de services (#[AsDecorator]) et le HttpCache qui enveloppe le noyau.
AdapterLes nombreux bridges (Monolog, Doctrine, Twig…) qui adaptent des bibliothèques tierces aux interfaces de Symfony.
Chain of ResponsibilityLa chaîne de middlewares de Messenger et de HttpKernel : chaque maillon traite ou passe au suivant.
🔑 Pourquoi ça compte pour toi : quand tu lis « ce service décore le cache » ou « ajoute un voter » dans une doc, tu sais désormais exactement ce que ça implique — quelle interface respecter, comment étendre sans casser. Le vocabulaire des patterns transforme une documentation opaque en instructions limpides. C'est là toute leur valeur au quotidien.

Remarque aussi que ces patterns ne sont pas isolés : l'EventDispatcher (Observer) sert à implémenter des points d'extension, eux-mêmes souvent des Strategy ; le middleware (Chain of Responsibility) transporte des Command. Les patterns se combinent — un framework mûr, c'est une conversation entre eux.

09 — ANTI-PATTERNS & PIÈGES

Le revers : quand un pattern aggrave les choses

Le danger n'est pas d'ignorer les patterns, c'est d'en abuser. Un pattern posé sans problème à résoudre est de la complexité gratuite — et certains « patterns » sont carrément des pièges.

Apprendre les patterns crée une tentation redoutable : les appliquer partout, pour prouver qu'on les connaît. C'est le chemin le plus sûr vers un code plus difficile à lire que celui qu'on prétendait améliorer. Quelques garde-fous s'imposent.

La « pattern soup »
Empiler factory, strategy, decorator et observer sur un problème trivial. Chaque pattern ajoute une indirection ; cumulés sans nécessité, ils transforment un flux linéaire lisible en labyrinthe d'interfaces. La règle : un pattern doit retirer plus de complexité qu'il n'en ajoute.
Le Singleton, souvent un anti-pattern
Garantir une instance unique accessible globalement semble pratique, mais crée un état global déguisé : dépendances cachées, tests quasi impossibles à isoler, couplage fort. Dans une application moderne, l'injection de dépendances (une seule instance gérée par le conteneur) rend presque tous les Singletons inutiles.
L'abstraction spéculative
Introduire une interface et une factory « au cas où on aurait un jour une deuxième implémentation ». Ce jour n'arrive souvent jamais, et tu as payé la complexité d'avance. Le principe YAGNI (You Aren't Gonna Need It) : abstrais quand le besoin se matérialise, pas avant.
🔑 Le bon réflexe : partir du problème, pas du pattern. Ne te demande jamais « quel pattern appliquer ici ? » mais « quel problème concret ai-je ? ». Si un if/else te suffit et reste clair, garde-le. Le pattern arrive quand la douleur est réelle : un switch qui gonfle à chaque feature (→ Strategy), des combinaisons d'options qui explosent (→ Decorator), un objet qui connaît trop de monde (→ Observer).

Une manière saine de voir les choses : les patterns sont un vocabulaire de refactoring, pas un plan de construction. On écrit d'abord la solution la plus simple, on la laisse révéler ses tensions, et on introduit un pattern quand le code lui-même réclame cette structure. Le pattern nomme et cristallise une amélioration ; il ne la précède pas.

💡 Le rappel SOLID : presque tous les pièges ci-dessus violent en réalité un principe SOLID mal compris — sur-appliquer l'inversion de dépendance donne des abstractions inutiles, sur-appliquer l'ouverture/fermeture donne des indirections partout. SOLID est un guide de jugement, pas un quota à remplir.
10 — SYNTHÈSE

Synthèse : un vocabulaire au service du jugement

Les patterns ne remplacent pas le discernement — ils l'outillent. Voici comment choisir, et le réflexe à garder au-dessus de tous les autres.

On a parcouru les principes SOLID, puis six patterns concrets, avant de voir Symfony en être tissé et de mesurer le danger de l'excès. Récapitulons non pas des recettes, mais une façon de penser.

Les patterns vus, en une phrase chacun

PatternRésoutEn une phrase
Strategyalgorithmes interchangeablesplusieurs façons de faire, choisies à l'exécution.
Factorycréation coupléedéléguer le new pour parler d'abstractions.
Decoratoroptions combinatoiresemballer pour ajouter, empilable à l'infini.
Observerréactions multiplesdiffuser un fait à des abonnés qu'on ne connaît pas.
Commandactions réifiéesl'action devient objet : file, journal, undo.
Adapterinterfaces incompatiblestraduire pour réconcilier sans réécrire.

Comment choisir

Un switch qui gonfle
→ Strategy
Options qui explosent
→ Decorator
Un objet qui en connaît trop
→ Observer
Besoin d'undo / de file
→ Command
🔑 Le réflexe au-dessus de tous : pars du problème, jamais du pattern. La solution la plus simple qui marche et reste lisible est la bonne, jusqu'à preuve du contraire. Le pattern se mérite quand le code, de lui-même, réclame sa structure. SOLID t'aide à entendre cette demande ; les patterns te donnent les mots pour y répondre — et un vocabulaire commun pour en parler à ton équipe.

Où ça se relie

💡 Bibliographie : Gamma, Helm, Johnson & Vlissides, Design Patterns (« Gang of Four », 1994) ; Robert C. Martin, Clean Code et Agile Software Development (principes SOLID) ; Martin Fowler, Refactoring ; le site Refactoring.Guru pour des illustrations pas à pas ; la documentation Symfony (composants comme catalogue de patterns).