Aller au contenu principal

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

Oubliez la théorie universitaire. Découvrez comment Factory, Observer et Strategy sauvent votre code Symfony au quotidien.

Publié le

Temps de lecture 5 min

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.

Gestion des cookies

J'utilise Google Analytics, Google Tag Manager et Microsoft Clarity pour améliorer votre expérience. Vous pouvez choisir les services que vous autorisez.

Le code est dans la boîte !

Vous recevrez bientôt les nouveaux billets dans votre boîte mail. Pas de spam, promis.

Désinscription possible à tout moment.