· 14 min read

Tutoriel : Le Design Pattern Strategy dans Symfony (pour les débutants)

Découvrez comment faire évoluer votre code Symfony de l'approche naïve avec des if/else vers le design pattern Strategy. Tutoriel complet avec exemples concrets.

Découvrez comment faire évoluer votre code Symfony de l'approche naïve avec des if/else vers le design pattern Strategy. Tutoriel complet avec exemples concrets.

🎯 Objectif du tutoriel

Dans ce tutoriel, nous allons créer un système d’analyse de céréales en Symfony et voir comment évoluer progressivement notre code :

  1. Étape 1 : Approche naïve avec des if/else
  2. Étape 2 : Amélioration avec l’expression match (PHP 8+)
  3. Étape 3 : Solution professionnelle avec le Design Pattern Strategy

📋 Besoin métier : analyser des céréales

Contexte : Nous développons une application pour une coopérative agricole qui doit analyser différents types de céréales et fournir des informations nutritionnelles spécifiques.

Types de céréales à analyser :

  • Blé
  • Orge
  • Tournesol

🚨 Défi majeur : Les types d’analyse vont grandir de manière imprévisible ! Les métiers vont ajouter de nouveaux types de céréales au fur et à mesure :

  • Maïs
  • Colza
  • Soja
  • Et bien d’autres… que nous ne connaissons pas encore !

Objectif : Créer un système flexible qui peut facilement s’étendre pour de nouveaux types de céréales sans modifier le code existant. Chaque ajout doit être simple et ne pas casser les fonctionnalités existantes.

🎤 Inspirations et contexte

Ce tutoriel s’inspire des conférences de l’API Platform Conference 2025 sur les Design Patterns et de l’injection de dépendances dans Symfony :

Ces conférences m’ont motivé à créer ce tutoriel pratique pour démontrer concrètement comment implémenter le Design Pattern Strategy dans Symfony, en montrant l’évolution progressive du code.


🚀 Étape 0 : préparation du projet Symfony

Prérequis : Pour suivre ce tutoriel, vous devez avoir un projet Symfony fonctionnel. Si vous n’en avez pas encore, créez-en un avec la commande suivante :

symfony new mon_projet_strategy --webapp
cd mon_projet_strategy

📚 Qu’est-ce qu’un service dans Symfony ?

Concept fondamental : Un service dans Symfony est une classe PHP qui effectue une tâche spécifique (envoyer un email, analyser des données, etc.).

💡 Analogie simple : Imaginez une coopérative agricole. Chaque service = un spécialiste agricole :

  • L’agronome (service) analyse les sols
  • Le technicien (service) vérifie les semences
  • Le comptable (service) gère les finances

Le conteneur de services est comme le directeur de la coopérative : il s’assure que chaque spécialiste a ce dont il a besoin pour travailler.

Avantages des services :

  • Réutilisables : Un service peut être utilisé partout dans l’application
  • Testables : Chaque service peut être testé indépendamment
  • Injection automatique : Symfony injecte automatiquement les dépendances
  • Configuration centralisée : Tout est géré par le conteneur

Exemple simple :

// Un service = une classe avec une responsabilité
class EmailService 
{
    public function send(string $email, string $message): void 
    {
        // Logique d'envoi d'email
    }
}

Dans notre tutoriel, nous allons créer des services d’analyse de céréales et voir comment les organiser avec le pattern Strategy.


🚀 Étape 1 : approche naïve avec if/else

Approche naïve : Nous commençons par la méthode la plus simple et intuitive. Quand on débute en programmation, on utilise naturellement des if/else pour gérer différents cas. Cette approche fonctionne parfaitement pour un petit nombre de cas, mais révèle rapidement ses limites quand le système grandit.

Service d’analyse avec if/else

Concept clé à comprendre : Dans cette approche, toute la logique de décision est concentrée dans une seule méthode. Chaque type de céréale est géré par une condition if/else séparée. C’est l’approche la plus intuitive mais qui pose des problèmes de maintenance.

Fichier : src/Service/If/CerealAnalyseIfService.php

<?php

namespace App\Service\If;

class CerealAnalyseIfService
{
    /**
     * Analyse un céréal en utilisant des if/else (approche traditionnelle)
     */
    public function analyserCereal(string $cereal): string
    {
        // Approche avec des if/else - plus difficile à maintenir et étendre
        if (strtolower($cereal) === 'ble') {
            return $this->analyserBle($cereal);
        } elseif (strtolower($cereal) === 'orge') {
            return $this->analyserOrge($cereal);
        } elseif (strtolower($cereal) === 'tournesol') {
            return $this->analyserTournesol($cereal);
        } else {
            throw new \InvalidArgumentException(
                sprintf('Aucune analyse disponible pour le céréal: %s', $cereal)
            );
        }
    }

    private function analyserBle(string $cereal): string
    {
        return "Analyse du blé (IF) : Riche en glucides et protéines, idéal pour la panification.";
    }

    private function analyserOrge(string $cereal): string
    {
        return "Analyse de l'orge (IF) : Excellente source de fibres, utilisée pour le malt et l'alimentation animale.";
    }

    private function analyserTournesol(string $cereal): string
    {
        return "Analyse du tournesol (IF) : Très riche en huile et vitamine E, parfait pour la production d'huile alimentaire.";
    }
}

Contrôleur pour l’approche if/else

Concept clé à comprendre : Le contrôleur utilise l’injection de dépendances pour récupérer notre service d’analyse. Il teste différents types de céréales et gère les erreurs avec des blocs try/catch. Cette approche fonctionne mais nécessite de modifier le service à chaque ajout de nouveau type.

Fichier : src/Controller/DemoController.php

<?php

namespace App\Controller;

use App\Service\If\CerealAnalyseIfService;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Attribute\Route;

class DemoController extends AbstractController
{
    public function __construct(
        private readonly CerealAnalyseIfService $cerealAnalyseIfService
    ) {
    }

    #[Route('/if', name: 'app_demo_if')]
    public function demoIf(): Response
    {
        // Création d'exemples de céréales
        $cereals = [
            'Ble',
            'Orge', 
            'Tournesol',
            'Colza', // Céréal sans analyse
        ];

        $analyses = [];

        // Analyse de chaque céréal en utilisant des if/else
        foreach ($cereals as $cereal) {
            try {
                $analyse = $this->cerealAnalyseIfService->analyserCereal($cereal);
                $analyses[] = [
                    'cereal' => $cereal,
                    'analyse' => $analyse,
                    'success' => true,
                ];
            } catch (\InvalidArgumentException $e) {
                $analyses[] = [
                    'cereal' => $cereal,
                    'error' => $e->getMessage(),
                    'success' => false,
                ];
            }
        }

        return $this->render('demo/if.html.twig', [
            'analyses' => $analyses,
            'method' => 'if/else',
        ]);
    }
}

Test de l’approche if/else

Résultat : ✅ Fonctionne, mais…

❌ Problèmes de cette approche

  1. Violation du principe ouvert/fermé : Pour ajouter le colza, il faut modifier le code existant
    • 💡 Explication simple : C’est comme avoir un laboratoire d’analyse où ajouter une nouvelle céréale nécessite de refaire tous les protocoles !
  2. Difficile à maintenir : Toute la logique est concentrée dans une seule méthode
    • 💡 Problème : Si on veut changer l’analyse du blé, on risque de casser l’orge
  3. Risque d’erreurs : Facile d’oublier un cas ou de faire une erreur
    • 💡 Exemple : Oublier de gérer le “Maïs” dans la liste des conditions
  4. Tests complexes : Difficile de tester chaque cas individuellement
    • 💡 Pourquoi : On ne peut pas tester juste l’analyse du blé sans tester tout le service

🔄 Étape 2 : amélioration avec l’expression match (PHP 8+)

Approche moderne : Avec PHP 8+, l’expression match apporte une syntaxe plus élégante et performante que les if/else. C’est une amélioration significative qui rend le code plus lisible, mais qui ne résout pas les problèmes fondamentaux d’architecture. C’est un pas dans la bonne direction, mais pas encore la solution optimale.

Service d’analyse avec match

Concept clé à comprendre : L’expression match (PHP 8+) remplace les if/else par une syntaxe plus moderne et performante. Elle garantit l’exhaustivité (tous les cas sont couverts) et retourne directement une valeur. Cependant, elle ne résout pas les problèmes architecturaux fondamentaux.

Fichier : src/Service/Match/CerealAnalyseMatchService.php

<?php

namespace App\Service\Match;

class CerealAnalyseMatchService
{
    /**
     * Analyse un céréal en utilisant l'expression match (PHP 8+)
     */
    public function analyserCereal(string $cereal): string
    {
        // Approche avec match - plus moderne que if/else mais toujours sans pattern
        return match (strtolower($cereal)) {
            'ble' => $this->analyserBle($cereal),
            'orge' => $this->analyserOrge($cereal),
            'tournesol' => $this->analyserTournesol($cereal),
            default => throw new \InvalidArgumentException(
                sprintf('Aucune analyse disponible pour le céréal: %s', $cereal)
            ),
        };
    }

    private function analyserBle(string $cereal): string
    {
        return "Analyse du blé (MATCH) : Riche en glucides et protéines, idéal pour la panification.";
    }

    private function analyserOrge(string $cereal): string
    {
        return "Analyse de l'orge (MATCH) : Excellente source de fibres, utilisée pour le malt et l'alimentation animale.";
    }

    private function analyserTournesol(string $cereal): string
    {
        return "Analyse du tournesol (MATCH) : Très riche en huile et vitamine E, parfait pour la production d'huile alimentaire.";
    }
}

Mise à jour du contrôleur

Concept clé à comprendre : Le contrôleur est mis à jour pour utiliser le nouveau service match. L’injection de dépendances permet d’avoir plusieurs services dans le même contrôleur. Cette approche reste limitée car elle nécessite toujours de modifier le code pour ajouter de nouveaux types.

<?php

namespace App\Controller;

use App\Service\If\CerealAnalyseIfService;
use App\Service\Match\CerealAnalyseMatchService;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Attribute\Route;

class DemoController extends AbstractController
{
    public function __construct(
        private readonly CerealAnalyseIfService $cerealAnalyseIfService,
        private readonly CerealAnalyseMatchService $cerealAnalyseMatchService
    ) {
    }

    #[Route('/match', name: 'app_demo_match')]
    public function demoMatch(): Response
    {
        // Création d'exemples de céréales
        $cereals = [
            'Ble',
            'Orge',
            'Tournesol', 
            'Colza', // Céréal sans analyse
        ];

        $analyses = [];

        // Analyse de chaque céréal en utilisant l'expression match
        foreach ($cereals as $cereal) {
            try {
                $analyse = $this->cerealAnalyseMatchService->analyserCereal($cereal);
                $analyses[] = [
                    'cereal' => $cereal,
                    'analyse' => $analyse,
                    'success' => true,
                ];
            } catch (\InvalidArgumentException $e) {
                $analyses[] = [
                    'cereal' => $cereal,
                    'error' => $e->getMessage(),
                    'success' => false,
                ];
            }
        }

        return $this->render('demo/match.html.twig', [
            'analyses' => $analyses,
            'method' => 'match',
        ]);
    }
}

Test de l’approche match

Résultat : ✅ Syntaxe plus moderne, mais mêmes problèmes fondamentaux

✅ Avantages de l’expression match

  1. Syntaxe moderne : Plus lisible que les if/else
  2. Expression : Retourne directement une valeur
  3. Exhaustivité : Garantit qu’on couvre tous les cas
  4. Performance : Optimisée par PHP

⚠️ Mais toujours les mêmes problèmes

  1. Violation du principe ouvert/fermé : Toujours besoin de modifier le code
  2. Maintenabilité limitée : Toute la logique reste concentrée
  3. Testabilité : Difficile de tester chaque cas individuellement

✅ Étape 3 : solution professionnelle avec le design pattern strategy

Approche professionnelle : Le Design Pattern Strategy est la solution architecturale qui respecte les principes SOLID et permet une évolution naturelle du code. Chaque type de céréale devient une classe indépendante, facilement testable et extensible. C’est la force du pattern : ajouter une nouvelle fonctionnalité sans modifier le code existant, garantissant la stabilité et la maintenabilité.

Création de l’interface

Concept clé à comprendre : L’interface définit le contrat que toutes les stratégies doivent respecter. L’attribut #[AutoconfigureTag] est la clé : il fait que Symfony tague automatiquement toutes les classes qui implémentent cette interface. C’est le fondement du pattern Strategy.

Fichier : src/Service/Strategy/AnalyseCerealInterface.php

<?php

declare(strict_types=1);

namespace App\Service\Strategy;

use Symfony\Component\DependencyInjection\Attribute\AutoconfigureTag;

#[AutoconfigureTag('app.analyse_cereal')]
interface AnalyseCerealInterface
{
    public function analyse(string $cereal): string;
    public function supports(string $cereal): bool;
}

Création des stratégies concrètes

Concept clé à comprendre : Chaque stratégie est une classe indépendante qui implémente l’interface. La méthode supports() détermine si cette stratégie peut traiter le type de céréale donné. C’est le principe de responsabilité unique : une classe = une responsabilité.

Fichier : src/Service/Strategy/BleAnalyse.php

<?php

declare(strict_types=1);

namespace App\Service\Strategy;

class BleAnalyse implements AnalyseCerealInterface
{
    public function supports(string $cereal): bool
    {
        return strtolower($cereal) === 'ble';
    }

    public function analyse(string $cereal): string
    {
        return "Analyse du blé : Riche en glucides et protéines, idéal pour la panification.";
    }
}

Fichier : src/Service/Strategy/OrgeAnalyse.php

<?php

declare(strict_types=1);

namespace App\Service\Strategy;

class OrgeAnalyse implements AnalyseCerealInterface
{
    public function supports(string $cereal): bool
    {
        return strtolower($cereal) === 'orge';
    }

    public function analyse(string $cereal): string
    {
        return "Analyse de l'orge : Excellente source de fibres, utilisée pour le malt et l'alimentation animale.";
    }
}

Fichier : src/Service/Strategy/TournesolAnalyse.php

<?php

declare(strict_types=1);

namespace App\Service\Strategy;

class TournesolAnalyse implements AnalyseCerealInterface
{
    public function supports(string $cereal): bool
    {
        return strtolower($cereal) === 'tournesol';
    }

    public function analyse(string $cereal): string
    {
        return "Analyse du tournesol : Très riche en huile et vitamine E, parfait pour la production d'huile alimentaire.";
    }
}

Création du manager

Concept clé à comprendre : Le manager est le cœur du pattern Strategy. Il utilise #[AutowireIterator] pour récupérer automatiquement toutes les stratégies taguées. Il parcourt les stratégies jusqu’à trouver celle qui peut traiter le type de céréale.

💡 Analogie simple : Le manager est comme un directeur de laboratoire qui a plusieurs techniciens spécialisés. Quand on lui demande une analyse, il va voir quel technicien peut la réaliser et lui confie la tâche.

Fichier : src/Service/Strategy/CerealAnalyseManager.php

<?php

declare(strict_types=1);

namespace App\Service\Strategy;

use Symfony\Component\DependencyInjection\Attribute\AutowireIterator;

class CerealAnalyseManager
{
    public function __construct(
        /** @var iterable<AnalyseCerealInterface> */
        #[AutowireIterator(tag: 'app.analyse_cereal')]
        private iterable $strategies
    ) {
    }

    /**
     * Analyse un céréal en utilisant la stratégie appropriée
     */
    public function analyserCereal(string $cereal): string
    {
        foreach ($this->strategies as $strategy) {
            if ($strategy->supports($cereal)) {
                return $strategy->analyse($cereal);
            }
        }

        throw new \InvalidArgumentException(
            sprintf('Aucune stratégie d\'analyse trouvée pour le céréal: %s', $cereal)
        );
    }
}

🏷️ Les attributs Symfony : la magie moderne

Maintenant que nous avons vu le manager en action, expliquons les attributs PHP modernes qui rendent cette implémentation si élégante. Ces attributs remplacent les anciens fichiers de configuration XML/YAML et rendent le code plus lisible et maintenable.

#[AutoconfigureTag] - Tagger automatiquement les services

L’attribut #[AutoconfigureTag('app.analyse_cereal')] sur notre interface fait que toutes les classes qui implémentent cette interface sont automatiquement taguées avec app.analyse_cereal.

💡 Explication simple : C’est comme mettre une étiquette “TECHNICIEN CÉRÉALES” sur tous les spécialistes qui savent analyser les céréales. Symfony fait ça automatiquement !

#[AutowireIterator] - Récupérer tous les services tagués

Dans notre manager, l’attribut #[AutowireIterator(tag: 'app.analyse_cereal')] permet d’injecter automatiquement tous les services tagués dans une collection.

💡 Explication simple : C’est comme dire au directeur “donne-moi la liste de tous les techniciens céréales” - Symfony s’occupe de tout :

public function __construct(
    /** @var iterable<AnalyseCerealInterface> */
    #[AutowireIterator(tag: 'app.analyse_cereal')]
    private iterable $strategies
) {
}

Avantages des attributs modernes

Configuration déclarative : Tout est visible dans le code
Type safety : L’IDE comprend les types injectés
Auto-complétion : Plus d’erreurs de configuration
Maintenance simplifiée : Pas de fichiers de config séparés
Évolutivité : Ajouter une nouvelle stratégie = créer une classe, c’est tout !

Ces attributs représentent l’évolution moderne de Symfony vers une approche plus déclarative et moins verbeuse.

Mise à jour du contrôleur

Concept clé à comprendre : Le contrôleur final utilise le manager Strategy qui gère automatiquement toutes les stratégies. L’injection de dépendances permet d’avoir les trois approches dans le même contrôleur pour comparaison. Le manager s’occupe de tout, le contrôleur reste simple.

<?php

namespace App\Controller;

use App\Service\If\CerealAnalyseIfService;
use App\Service\Match\CerealAnalyseMatchService;
use App\Service\Strategy\CerealAnalyseManager;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Attribute\Route;

class DemoController extends AbstractController
{
    public function __construct(
        private readonly CerealAnalyseIfService $cerealAnalyseIfService,
        private readonly CerealAnalyseMatchService $cerealAnalyseMatchService,
        private readonly CerealAnalyseManager $cerealAnalyseManager
    ) {
    }

    #[Route('/pattern', name: 'app_demo_pattern')]
    public function demoPattern(): Response
    {
        // Création d'exemples de céréales
        $cereals = [
            'Ble',
            'Orge',
            'Tournesol',
            'Colza', // Céréal sans stratégie d'analyse
        ];

        $analyses = [];

        // Analyse de chaque céréal en utilisant le pattern Strategy
        foreach ($cereals as $cereal) {
            try {
                $analyse = $this->cerealAnalyseManager->analyserCereal($cereal);
                $analyses[] = [
                    'cereal' => $cereal,
                    'analyse' => $analyse,
                    'success' => true,
                ];
            } catch (\InvalidArgumentException $e) {
                $analyses[] = [
                    'cereal' => $cereal,
                    'error' => $e->getMessage(),
                    'success' => false,
                ];
            }
        }

        return $this->render('demo/pattern.html.twig', [
            'analyses' => $analyses,
            'strategies_count' => 3, // Nombre fixe de stratégies disponibles
        ]);
    }
}

Test du pattern strategy

Résultat : ✅ Solution professionnelle qui respecte les principes SOLID

🎯 Avantages du pattern strategy

  1. ✅ Respect du principe ouvert/fermé : Ouvert à l’extension, fermé à la modification
  2. ✅ Extensibilité : Facile d’ajouter de nouveaux types de céréales
  3. ✅ Maintenabilité : Chaque stratégie est isolée et indépendante
  4. ✅ Testabilité : Chaque stratégie peut être testée individuellement
  5. ✅ Injection de dépendances : Symfony gère automatiquement l’injection

🚀 Ajouter une nouvelle stratégie (exemple : maïs)

Concept clé à comprendre : C’est là que la magie du pattern Strategy opère ! Pour ajouter un nouveau type de céréale, il suffit de créer une nouvelle classe qui implémente l’interface. Symfony détecte automatiquement la nouvelle stratégie grâce aux attributs. Aucune modification du code existant n’est nécessaire.

Pour ajouter l’analyse du maïs, il suffit de créer une nouvelle classe :

Fichier : src/Service/Strategy/MaisAnalyse.php

<?php

declare(strict_types=1);

namespace App\Service\Strategy;

class MaisAnalyse implements AnalyseCerealInterface
{
    public function supports(string $cereal): bool
    {
        return strtolower($cereal) === 'mais';
    }

    public function analyse(string $cereal): string
    {
        return "Analyse du maïs : Riche en amidon, utilisé pour l'alimentation animale et l'éthanol.";
    }
}

C’est tout ! Symfony détecte automatiquement la nouvelle stratégie et l’injecte dans le manager.


📊 Comparaison des trois approches

CritèreIf/ElseMatch (PHP 8+)Strategy Pattern
Syntaxe❌ Verbose✅ Moderne✅ Professionnelle
Maintenabilité❌ Difficile⚠️ Limitée✅ Excellente
Extensibilité❌ Modification requise❌ Modification requise✅ Ajout simple
Testabilité❌ Complexe❌ Complexe✅ Parfaite
Principe Ouvert/Fermé❌ Violé❌ Violé✅ Respecté
Injection de dépendances❌ Manuelle❌ Manuelle✅ Automatique

🎯 Résumé du tutoriel

Ce que nous avons appris :

  1. Étape 1 - If/Else : Approche naïve qui fonctionne mais pose des problèmes de maintenance
  2. Étape 2 - Match : Amélioration syntaxique mais mêmes problèmes fondamentaux
  3. Étape 3 - Strategy Pattern : Solution professionnelle qui respecte les principes SOLID

Points clés du pattern strategy :

  • Interface commune : AnalyseCerealInterface avec analyse() et supports()
  • Stratégies concrètes : Une classe par type de céréal
  • Manager : Utilise les stratégies sans les connaître
  • Injection automatique : Symfony gère tout avec les attributs

Avantages concrets :

Ajouter une nouvelle céréale = Créer une nouvelle classe
Code existant intact = Pas de régression
Tests isolés = Chaque stratégie testable individuellement
Évolutivité = Architecture flexible et maintenable


Ressources utiles :


🎉 Félicitations ! Vous maîtrisez maintenant le Design Pattern Strategy dans Symfony. Cette approche vous servira dans tous vos projets pour créer du code maintenable et évolutif.

Back to Blog

Comments (0)

Loading comments...

Leave a Comment