Aller au contenu principal

Design patterns en PHP : les héros de l'ombre

Découvrez l'impact réel des Design Patterns en PHP et Symfony. Ces solutions éprouvées structurent et optimisent votre code au quotidien. Simplifiez votre développement!
Catégorie

PHP

Fonctionnalités du langage, évolutions récentes et techniques avancées en PHP 8.x.

Lecture
5 min
Niveau
Intermédiaire
mars 22 2025
Partager

On a souvent une image fausse des Design Patterns. On les voit comme des diagrammes UML poussiéreux qu'on apprend à l'école et qu'on oublie le lendemain. Ou comme des mots savants pour briller en entretien : "Ah oui, j'ai utilisé un Singleton ici."

En réalité, les Design Patterns sont les héros de l'ombre de votre application. Ce sont des solutions éprouvées à des problèmes que vous rencontrez tous les jours sans vous en rendre compte. Dans un projet Symfony, vous en utilisez déjà une dizaine.

Regardons comment ils structurent secrètement notre code.

Factory : L'usine à objets complexes

Le problème : Créer un objet est parfois un cauchemar. Il faut rassembler des données de cinq services différents, faire des conversions de types, gérer des valeurs par défaut... Si vous faites ça dans votre Controller, vous violez le principe de responsabilité unique. Votre Controller devient un ouvrier à la chaîne.

La solution : Une classe dédiée à la fabrication. Elle seule connaît la recette.

L'exemplet : SeoDtoFactory. Pour générer les métadonnées SEO d'une page, on ne va pas instancier manuellement le DTO avec ses 15 propriétés à chaque fois. On délègue ça à l'usine.

PHP
// src/Factory/SeoDtoFactory.php
final class SeoDtoFactory
{
    public function create(AbstractContent $content): SeoDto
    {
        // L'usine encapsule la complexité de la création
        // Elle sait comment gérer les titres vides, les descriptions manquantes...
        return new SeoDto(
            title: $content->getSeoTitle() ?? $content->getTitle(),
            description: $content->getSeoDescription() ?? $this->summarizer->summarize($content->getContent()),
            robots: $content->isIndexable() ? 'index, follow' : 'noindex',
            // ... et 10 autres propriétés
        );
    }
}

Dans mon code client (Controller), c'est limpide : $seo = $this->seoFactory->create($post);

2. Observer : le crieur public

Le problème : Quand je publie un article (Post), je veux prévenir Bing (IndexNow). Mais je ne veux PAS que mon PostController connaisse Bing. Pourquoi ? Parce que demain, je voudrai aussi prévenir Google, puis envoyer un Slack, puis purger le cache CDN. Si je mets tout ça dans le Controller, je crée un God Controller obèse et impossible à maintenir.

La solution : Observer (ou EventSubscriber dans Symfony). Le sujet (Post) crie "J'ai changé !" (Event), et les observateurs réagissent.

L'exemple : IndexNowSubscriber.

PHP
// src/EventSubscriber/IndexNowSubscriber.php
#[AsDoctrineListener(event: Events::postPersist)]
#[AsDoctrineListener(event: Events::postUpdate)]
final class IndexNowSubscriber
{
    public function postPersist(LifecycleEventArgs $args): void
    {
        $entity = $args->getObject();
        
        // Si ce n'est pas un contenu, je dors
        if (!$entity instanceof AbstractContent) {
            return;
        }

        // Sinon, je notifie Bing !
        $this->indexNowService->submit($entity->getUrl());
    }
}

C'est magique : le code qui sauvegarde l'article ($em->flush()) ne sait même pas que Bing existe. C'est le découplage parfait.

Facade : le réceptionniste

Le problème : Vous avez un sous-système complexe. Pour nettoyer un contenu, vous devez appeler le DomSanitizer, puis le SeoSanitizer, puis le KeywordOptimizer. Si vous devez appeler ces 3 services partout, vous créez une dépendance forte et répétitive.

La solution : Une Facade. Une interface unique et simplifiée qui masque la complexité derrière elle.

L'exemple : ContentSanitizer.

PHP
// src/Service/Security/ContentSanitizer.php
/**
 * Facade pattern delegating to specialized sanitizers.
 */
final readonly class ContentSanitizer implements ContentSanitizerInterface
{
    public function __construct(
        private DomSanitizer $domSanitizer,
        private SeoSanitizer $seoSanitizer,
    ) {}

    public function sanitizeHtml(string $html): string
    {
        // Le client ne sait pas que c'est DomSanitizer qui bosse
        return $this->domSanitizer->sanitizeHtml($html);
    }

    public function sanitizeSeoData(array $data): array
    {
        // Le client ne sait pas que c'est SeoSanitizer qui bosse
        return $this->seoSanitizer->sanitizeSeoData($data);
    }
}

Pour le développeur qui utilise ce service, c'est simple : "Je veux nettoyer du contenu". Il n'a pas besoin de connaître l'organisation interne de vos sanitizers.

Decorator : l'armure d'Iron Man

Le problème : Vous avez une tâche planifiée (ScheduledTask). Vous voulez qu'elle ne s'exécute pas sur deux serveurs en même temps (Lock Distribué). Vous pourriez modifier la classe de la tâche pour ajouter la logique de Lock. Mais alors vous mélangez le "Quoi" (la tâche) et le "Comment" (le lock). Et vous devrez copier-coller ce code dans 50 tâches.

La solution : Decorator (ou ici, son cousin via Trait/Composition). On ajoute un comportement autour de la méthode sans la modifier.

L'exemple : WithDistributedLock.

PHP
trait WithDistributedLock
{
    protected function executeWithLock(callable $callback, float $ttl): void
    {
        $lock = $this->factory->createLock($this->getLockKey());

        if (!$lock->acquire()) {
            $this->logger->warning('Tâche déjà en cours, j\'abandonne.');
            return;
        }

        try {
            $callback(); // On exécute la vraie tâche protégée
        } finally {
            $lock->release();
        }
    }
}

C'est comme enfiler une armure. Le bonhomme à l'intérieur (votre logique métier) reste le même, mais il a gagné une nouvelle capacité (l'invulnérabilité aux conditions de course) sans changer d'ADN.

Singleton : l'unique

Le problème : Je veux être sûr d'avoir une seule instance de ma connexion à la base de données. Je ne veux pas ouvrir 50 ports MySQL à chaque requête.

La solution : Le Singleton. Une classe qui ne s'instancie qu'une seule fois.

la réalité Symfony : Vous n'écrivez jamais de Singleton (private static $instance). C'est le Service Container de Symfony qui gère ça pour vous. Par défaut, tous vos services (Mailer, Repository, Factory) sont des Singletons. Quand vous demandez le MailerService à trois endroits différents, Symfony vous donne exactement la même instance.

Le mot de la fin

Les Design Patterns ne sont pas de la théorie pour architectes en tour d'ivoire. Ce sont des outils pragmatiques pour résoudre des problèmes de "la vraie vie" :

  • Factory : Pour ne pas répéter la logique de création (new).
  • Observer : Pour déconnecter les composants (Events).
  • Facade : Pour simplifier l'utilisation d'un système complexe.
  • Decorator : Pour ajouter des super-pouvoirs (Logs, Cache, Lock) sans toucher au code.

Vous les utilisiez déjà sans le savoir. Maintenant, vous pouvez les nommer. Et nommer les choses, c'est le début de la maîtrise.

Poursuivre la lecture

Sélectionné avec soin pour vous.

DX

Les délimiteurs Twig : ce problème d'espace blanc que vous ignorez

Gaps inline-block, diffs bruyants, layouts instables : comprenez l'impact des délimiteurs Twig sur l'espace blanc et adoptez les bonnes pratiques avec {%- et {{-.

8 min de lecture
UX

Le FOUC n'est pas un bug graphique, c'est une faille d'architecture !

Marre du contenu qui saute au chargement ? Découvrez comment éradiquer le FOUC (Flash of Unstyled Content), améliorer votre score CLS et optimiser l'UX de vos applications web modernes.

4 min de lecture
UX

Symfony UX Icons : 200 000 icônes SVG sans CDN, sans sprite, sans prise de tête

Fini les sprites et Font Awesome ! Découvrez comment Symfony UX Icons intègre 200k+ icônes (Iconify) nativement. Zéro requête HTTP, performances maximales.

6 min de lecture