🔧 Services et Injection de Dépendances

Le cœur de Symfony

DI • Autowiring • Service Container • Configuration

Formation DWWM - 2026

Au programme 📋

# 🎯 Partie 1

Qu'est-ce qu'un Service ?

Objets réutilisables et partagés

Définition d'un service 🔧

Service = Objet PHP qui effectue une tâche spécifique et qui peut être réutilisé partout dans l'application.

Analogie : La boîte à outils 🧰

Un service = un outil dans ta boîte à outils :

Tu n'achètes pas un nouveau marteau à chaque fois que tu dois planter un clou ! Tu utilises le MÊME marteau (même instance de service) partout.

Exemples de services Symfony 🛠️

Service Rôle Utilisation
EntityManager Gérer la base de données $em->persist($entity)
Mailer Envoyer des emails $mailer->send($email)
Logger Écrire des logs $logger->info('Message')
PasswordHasher Hacher les mots de passe $hasher->hashPassword($user, $password)
Router Générer des URLs $router->generate('route_name')
Twig Moteur de templates $twig->render('template.html.twig')
HttpClient Requêtes HTTP $client->request('GET', $url)

Service vs Objet normal 🆚

🔧 Service (Singleton)

Une seule instance partagée

  • Créé une fois au démarrage
  • Réutilisé partout
  • Géré par le conteneur
  • Injection automatique
// Même instance partout
$logger = $container->get(Logger::class);
$logger2 = $container->get(Logger::class);
// $logger === $logger2 → TRUE

📦 Objet normal

Nouvelle instance à chaque fois

  • Créé avec new
  • Nouvelle instance à chaque utilisation
  • Géré manuellement
  • Passage manuel en paramètres
// Nouvelles instances
$user1 = new User();
$user2 = new User();
// $user1 === $user2 → FALSE

💡 Règle simple :

# 💉 Partie 2

Injection de Dépendances

Dependency Injection (DI)

Le problème sans injection de dépendances ❌

Code couplé et difficile à tester

class ArticleController
{
    public function send()
    {
        // ❌ MAUVAIS : Création directe des dépendances
        $logger = new Logger();
        $mailer = new Mailer();
        
        $logger->info('Envoi email...');
        $mailer->send($email);
        
        // Problèmes :
        // 1. Couplage fort (dépend directement de Logger et Mailer)
        // 2. Impossible de tester sans vraiment logger/envoyer d'email
        // 3. Duplication si utilisé ailleurs
        // 4. Pas de contrôle sur la configuration
    }
}

⚠️ Problèmes du couplage fort :

La solution : Injection de dépendances ✅

Code découplé et testable

class ArticleController
{
    public function send(
        LoggerInterface $logger,      // ← Injecté par Symfony
        MailerInterface $mailer       // ← Injecté par Symfony
    ) {
        $logger->info('Envoi email...');
        $mailer->send($email);
        
        // Avantages :
        // 1. Pas de "new" → Symfony fournit les instances
        // 2. Facile à tester (on peut injecter des mocks)
        // 3. Une seule configuration centralisée
        // 4. Changement de Logger ? Un seul endroit à modifier
    }
}

✅ Avantages de l'injection de dépendances :

Analogie de l'injection de dépendances 🏭

Le restaurant et ses cuisiniers 👨‍🍳

Sans injection (mauvais) :

Avec injection (bon) :

Types d'injection de dépendances 📍

1. Injection par constructeur (recommandée)

class ArticleService
{
    private LoggerInterface $logger;

    public function __construct(LoggerInterface $logger)
    {
        $this->logger = $logger;
    }

    public function publish()
    {
        $this->logger->info('Article publié');
    }
}

✅ Avantage : Dépendances obligatoires, impossible de créer l'objet sans elles.

2. Injection par méthode (pour contrôleurs)

class ArticleController extends AbstractController
{
    #[Route('/article/send')]
    public function send(LoggerInterface $logger)  // ← Injection dans la méthode
    {
        $logger->info('Article envoyé');
    }
}

✅ Avantage : Injection uniquement quand nécessaire.

3. Injection par setter (optionnel, rare)

class ArticleService
{
    private ?LoggerInterface $logger = null;

    public function setLogger(LoggerInterface $logger): void
    {
        $this->logger = $logger;
    }
}

⚠️ Attention : Dépendance optionnelle, risque de null.

# 🤖 Partie 3

Autowiring Automatique

Injection automatique par type

C'est quoi l'autowiring ? 🤖

Autowiring = Symfony devine automatiquement quel service injecter en se basant sur le TYPE du paramètre.

Analogie : Le majordome intelligent 🎩

L'autowiring = un majordome qui devine tes besoins :

Symfony regarde le type-hint et injecte automatiquement le bon service !

Autowiring en action 🎯

Exemple simple

use Psr\Log\LoggerInterface;
use Symfony\Component\Mailer\MailerInterface;
use Doctrine\ORM\EntityManagerInterface;

class ArticleController extends AbstractController
{
    #[Route('/article/publish')]
    public function publish(
        LoggerInterface $logger,              // ← Autowiring !
        MailerInterface $mailer,              // ← Autowiring !
        EntityManagerInterface $em            // ← Autowiring !
    ): Response {
        // Symfony a automatiquement injecté les bons services
        
        $logger->info('Publication en cours...');
        $mailer->send($email);
        $em->flush();
        
        return new Response('Publié !');
    }
}

✅ Ce qui se passe :

  1. Symfony voit LoggerInterface $logger
  2. Cherche dans le conteneur un service qui implémente LoggerInterface
  3. Trouve le service de log configuré
  4. L'injecte automatiquement

Pourquoi l'autowiring fonctionne ? 🔍

Grâce aux type-hints PHP

// Symfony analyse les types
public function send(
    LoggerInterface $logger,        // Type = LoggerInterface
    MailerInterface $mailer,        // Type = MailerInterface
    string $message                 // Type = string → IGNORÉ (pas un service)
)

Règles de l'autowiring

Situation Résultat
Un seul service du type demandé ✅ Injection automatique
Plusieurs services du même type ❌ Erreur (ambiguïté)
Aucun service du type demandé ❌ Erreur (service introuvable)
Type scalaire (string, int, bool...) ⏭️ Ignoré (pas un service)

💡 Par défaut dans Symfony :

L'autowiring est ACTIVÉ par défaut pour toutes vos classes dans src/ !

# 📦 Partie 4

Service Container

Le registre central des services

C'est quoi le Service Container ? 📦

Service Container = Grosse boîte qui contient TOUS les services de l'application et sait comment les créer.

Analogie : L'entrepôt automatisé 🏭

Le Service Container = un entrepôt Amazon ultra-moderne :

Tu demandes "LoggerInterface", le robot va chercher le bon service et te le donne !

Structure du Service Container 🗂️

Service Container (ContainerInterface)
↓
┌─────────────────────────────────┐
│ Service: logger │
│ Class: Monolog\Logger │
│ Instance: [Logger object] │
├─────────────────────────────────┤
│ Service: mailer │
│ Class: Symfony\Mailer │
│ Instance: [Mailer object] │
├─────────────────────────────────┤
│ Service: doctrine.orm.em │
│ Class: EntityManager │
│ Instance: [EM object] │
└─────────────────────────────────┘

Le container sait :

Accéder aux services du container 🔑

Méthode 1 : Autowiring (recommandé)

public function send(LoggerInterface $logger)
{
    // Symfony injecte automatiquement
    $logger->info('Message');
}

Méthode 2 : Via le contrôleur (legacy)

class ArticleController extends AbstractController
{
    public function index(): Response
    {
        // ⚠️ Déprécié, utilisez l'autowiring à la place
        $logger = $this->container->get('logger');
        $logger->info('Message');
    }
}

Méthode 3 : Injection du container complet (à éviter)

use Symfony\Component\DependencyInjection\ContainerInterface;

public function send(ContainerInterface $container)
{
    // ❌ MAUVAIS : Service Locator anti-pattern
    $logger = $container->get(LoggerInterface::class);
}

⚠️ Toujours préférer l'autowiring !

Injecter le container complet masque les dépendances réelles et rend le code difficile à tester.

# ⚙️ Partie 5

Configuration services.yaml

Configurer les services

Le fichier services.yaml 📝

Fichier de configuration principal des services : config/services.yaml

Structure par défaut

# config/services.yaml
services:
    # Configuration par défaut pour tous les services
    _defaults:
        autowire: true              # Active l'autowiring
        autoconfigure: true         # Configure automatiquement les tags
        public: false               # Services privés par défaut

    # Enregistrement automatique de tous les services dans src/
    App\:
        resource: '../src/'
        exclude:
            - '../src/DependencyInjection/'
            - '../src/Entity/'
            - '../src/Kernel.php'

    # Configuration de services spécifiques
    # App\Service\MyService:
    #     arguments:
    #         $paramName: 'value'

Section _defaults expliquée 🔧

Option Description Valeur par défaut
autowire: true Injection automatique des dépendances true
autoconfigure: true Application automatique des tags true
public: false Services privés (non accessibles directement) false
bind: Lier des valeurs à des paramètres -

Pourquoi public: false ?

💡 Services privés vs publics :

Services privés = meilleure performance et code plus propre.

Auto-enregistrement des services 🤖

Section App\:

App\:
    resource: '../src/'
    exclude:
        - '../src/DependencyInjection/'
        - '../src/Entity/'
        - '../src/Kernel.php'

✅ Ce que ça fait :

Résultat

// Toute classe dans src/ devient automatiquement un service !
src/
├── Controller/
│   └── ArticleController.php     ← Service automatique
├── Service/
│   └── EmailSender.php            ← Service automatique
├── Repository/
│   └── ArticleRepository.php     ← Service automatique
└── Entity/
    └── Article.php                ← EXCLU (pas un service)
# 🏗️ Partie 6

Créer ses Propres Services

Services personnalisés

Quand créer un service personnalisé ? 🤔

Créez un service quand vous avez :

❌ NE créez PAS de service pour :

Créer un service simple 🏗️

Exemple : Service de calcul de prix

src/Service/PriceCalculator.php

<?php
namespace App\Service;

class PriceCalculator
{
    private const TVA_RATE = 0.20; // 20%
    
    /**
     * Calcule le prix TTC à partir du prix HT
     */
    public function calculateTTC(float $priceHT): float
    {
        return $priceHT * (1 + self::TVA_RATE);
    }
    
    /**
     * Calcule le montant de la TVA
     */
    public function calculateTVA(float $priceHT): float
    {
        return $priceHT * self::TVA_RATE;
    }
    
    /**
     * Applique une réduction
     */
    public function applyDiscount(float $price, float $discountPercent): float
    {
        return $price * (1 - $discountPercent / 100);
    }
}

✅ Résultat :

Le service est automatiquement enregistré grâce à App\: dans services.yaml !

Utiliser votre service personnalisé 🎯

Dans un contrôleur

namespace App\Controller;

use App\Service\PriceCalculator;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;

class ProductController extends AbstractController
{
    #[Route('/product/{id}')]
    public function show(
        int $id,
        PriceCalculator $calculator  // ← Autowiring automatique !
    ): Response {
        $product = // ... récupérer le produit
        
        $priceHT = 100.00;
        $priceTTC = $calculator->calculateTTC($priceHT);
        $priceWithDiscount = $calculator->applyDiscount($priceTTC, 10);
        
        return $this->render('product/show.html.twig', [
            'price_ht' => $priceHT,
            'price_ttc' => $priceTTC,
            'price_final' => $priceWithDiscount
        ]);
    }
}

Service avec dépendances 🔗

Exemple : Service d'envoi d'emails

src/Service/NotificationService.php

<?php
namespace App\Service;

use Psr\Log\LoggerInterface;
use Symfony\Component\Mailer\MailerInterface;
use Symfony\Component\Mime\Email;

class NotificationService
{
    public function __construct(
        private MailerInterface $mailer,        // ← Dépendance 1
        private LoggerInterface $logger         // ← Dépendance 2
    ) {}
    
    public function sendWelcomeEmail(string $to, string $username): void
    {
        $email = (new Email())
            ->from('noreply@example.com')
            ->to($to)
            ->subject('Bienvenue !')
            ->html("

Bonjour $username !

"); try { $this->mailer->send($email); $this->logger->info("Email de bienvenue envoyé à $to"); } catch (\Exception $e) { $this->logger->error("Erreur envoi email : " . $e->getMessage()); throw $e; } } public function sendOrderConfirmation(string $to, int $orderId): void { $email = (new Email()) ->from('noreply@example.com') ->to($to) ->subject('Commande confirmée') ->html("

Votre commande #$orderId est confirmée !

"); $this->mailer->send($email); $this->logger->info("Confirmation commande #$orderId envoyée à $to"); } }

💡 Autowiring en cascade :

Symfony injecte automatiquement MailerInterface et LoggerInterface dans le constructeur !

Service avec paramètres de configuration ⚙️

Service avec paramètres

namespace App\Service;

class EmailService
{
    public function __construct(
        private MailerInterface $mailer,
        private string $senderEmail,          // ← Paramètre
        private string $senderName            // ← Paramètre
    ) {}
    
    public function send(string $to, string $subject, string $content): void
    {
        $email = (new Email())
            ->from("$this->senderName <$this->senderEmail>")
            ->to($to)
            ->subject($subject)
            ->text($content);
        
        $this->mailer->send($email);
    }
}

Configuration dans services.yaml

services:
    App\Service\EmailService:
        arguments:
            $senderEmail: 'noreply@example.com'
            $senderName: 'Mon Application'

Ou avec des paramètres globaux

parameters:
    app.sender_email: 'noreply@example.com'
    app.sender_name: 'Mon Application'

services:
    App\Service\EmailService:
        arguments:
            $senderEmail: '%app.sender_email%'
            $senderName: '%app.sender_name%'
# 🔗 Partie 7

Interfaces et Services

Programmation orientée interface

Pourquoi utiliser des interfaces ? 🤔

Analogie : La prise électrique 🔌

Une interface = une prise électrique standardisée :

Tu peux changer d'appareil sans changer la prise !

✅ Avantages des interfaces :

Créer une interface et ses implémentations 🏗️

1. Créer l'interface

namespace App\Service\Payment;

interface PaymentProcessorInterface
{
    public function process(float $amount, string $currency): array;
    public function refund(string $transactionId): bool;
}

2. Implémentation Stripe

namespace App\Service\Payment;

class StripePaymentProcessor implements PaymentProcessorInterface
{
    public function __construct(
        private string $stripeApiKey
    ) {}
    
    public function process(float $amount, string $currency): array
    {
        // Logique Stripe
        return [
            'success' => true,
            'transaction_id' => 'stripe_' . uniqid(),
            'amount' => $amount
        ];
    }
    
    public function refund(string $transactionId): bool
    {
        // Logique de remboursement Stripe
        return true;
    }
}

3. Implémentation PayPal

namespace App\Service\Payment;

class PaypalPaymentProcessor implements PaymentProcessorInterface
{
    public function __construct(
        private string $paypalClientId,
        private string $paypalSecret
    ) {}
    
    public function process(float $amount, string $currency): array
    {
        // Logique PayPal
        return [
            'success' => true,
            'transaction_id' => 'paypal_' . uniqid(),
            'amount' => $amount
        ];
    }
    
    public function refund(string $transactionId): bool
    {
        // Logique de remboursement PayPal
        return true;
    }
}

Configurer quelle implémentation utiliser 🎯

Créer un alias dans services.yaml

services:
    # L'interface pointe vers l'implémentation Stripe
    App\Service\Payment\PaymentProcessorInterface:
        alias: App\Service\Payment\StripePaymentProcessor
    
    # Configuration de Stripe
    App\Service\Payment\StripePaymentProcessor:
        arguments:
            $stripeApiKey: '%env(STRIPE_API_KEY)%'
    
    # Configuration de PayPal (pas utilisé par défaut)
    App\Service\Payment\PaypalPaymentProcessor:
        arguments:
            $paypalClientId: '%env(PAYPAL_CLIENT_ID)%'
            $paypalSecret: '%env(PAYPAL_SECRET)%'

Utilisation dans le code

class OrderController extends AbstractController
{
    public function pay(
        PaymentProcessorInterface $paymentProcessor  // ← Reçoit StripePaymentProcessor
    ): Response {
        $result = $paymentProcessor->process(99.99, 'EUR');
        
        // Peut facilement changer pour PayPal en modifiant juste l'alias !
    }
}

✅ Avantage :

Pour passer de Stripe à PayPal, il suffit de changer l'alias dans services.yaml. Aucun code à modifier !

# 🏷️ Partie 8

Tags de Services

Grouper et identifier des services

C'est quoi un tag de service ? 🏷️

Tag = Étiquette qu'on met sur un service pour le marquer comme faisant partie d'un groupe.

Analogie : Les badges à un événement 🎫

Un tag = un badge de couleur à un événement :

Symfony regarde les badges pour savoir comment traiter chaque personne (service) !

Tags Symfony courants 🏷️

Tag Usage
kernel.event_listener Écouter des événements
kernel.event_subscriber S'abonner à des événements
console.command Commande console CLI
twig.extension Extension Twig (filtres, fonctions...)
doctrine.event_listener Écouter événements Doctrine
validator.constraint_validator Validateur de contrainte

💡 Autoconfigure :

Avec autoconfigure: true, Symfony ajoute automatiquement les bons tags en fonction des interfaces implémentées !

Créer un tag personnalisé 🏗️

Cas d'usage : Système de notifications multiples

1. Interface commune

namespace App\Service\Notification;

interface NotificationChannelInterface
{
    public function send(string $to, string $message): void;
    public function getName(): string;
}

2. Implémentations

// Email
class EmailNotification implements NotificationChannelInterface
{
    public function send(string $to, string $message): void
    {
        // Envoyer par email
    }
    
    public function getName(): string
    {
        return 'email';
    }
}

// SMS
class SmsNotification implements NotificationChannelInterface
{
    public function send(string $to, string $message): void
    {
        // Envoyer par SMS
    }
    
    public function getName(): string
    {
        return 'sms';
    }
}

// Push
class PushNotification implements NotificationChannelInterface
{
    public function send(string $to, string $message): void
    {
        // Envoyer notification push
    }
    
    public function getName(): string
    {
        return 'push';
    }
}

Utiliser les services taggés 🎯

3. Configuration du tag

services:
    # Tagger automatiquement toutes les classes qui implémentent NotificationChannelInterface
    _instanceof:
        App\Service\Notification\NotificationChannelInterface:
            tags: ['app.notification_channel']

4. Service qui utilise tous les canaux

namespace App\Service;

use App\Service\Notification\NotificationChannelInterface;

class NotificationManager
{
    /**
     * @param iterable<NotificationChannelInterface> $channels
     */
    public function __construct(
        private iterable $channels
    ) {}
    
    public function notify(string $channel, string $to, string $message): void
    {
        foreach ($this->channels as $notificationChannel) {
            if ($notificationChannel->getName() === $channel) {
                $notificationChannel->send($to, $message);
                return;
            }
        }
        
        throw new \Exception("Canal de notification '$channel' introuvable");
    }
    
    public function notifyAll(string $to, string $message): void
    {
        foreach ($this->channels as $channel) {
            $channel->send($to, $message);
        }
    }
}

5. Configuration de l'injection

services:
    App\Service\NotificationManager:
        arguments:
            $channels: !tagged_iterator app.notification_channel
# 🔍 Partie 9

Débugger les Services

Commandes utiles

Commandes de débogage 🔍

Lister tous les services

php bin/console debug:container

Chercher un service spécifique

php bin/console debug:container logger
php bin/console debug:container mailer
php bin/console debug:container doctrine

Voir les détails d'un service

php bin/console debug:container Psr\Log\LoggerInterface --show-arguments

Lister les services taggés

php bin/console debug:container --tag=kernel.event_listener

Voir les services autowirables

php bin/console debug:autowiring

# Chercher un type spécifique
php bin/console debug:autowiring Mailer

Vérifier la configuration

php bin/console lint:container

Exemple de sortie debug:container 📋

Commande

php bin/console debug:container logger

Résultat

Symfony Container Public Services
==================================

 Select one of the following services to display its information:
  [0] logger
  [1] monolog.logger
  [2] monolog.logger.cache
  [3] monolog.logger.console
  [4] monolog.logger.doctrine
  [5] monolog.logger.event
  [6] monolog.logger.mailer

> 0

Information for Service "logger"
=================================

 ---------------- -------------------------------------------- 
  Option           Value                                       
 ---------------- -------------------------------------------- 
  Service ID       logger                                      
  Class            Symfony\Bridge\Monolog\Logger               
  Tags             monolog.logger                              
                   psr_log.logger                              
  Public           no                                          
  Synthetic        no                                          
  Lazy             no                                          
  Shared           yes                                         
  Abstract         no                                          
  Autowired        no                                          
  Autoconfigured   no                                          
 ---------------- --------------------------------------------
# 🎓 Récapitulatif

Ce qu'on a appris

Concepts clés - Résumé 📚

Concept Description
Service Objet réutilisable qui fait une tâche spécifique
Injection de Dépendances Fournir à une classe ce dont elle a besoin
Autowiring Injection automatique basée sur les types
Service Container Registre central de tous les services
services.yaml Fichier de configuration des services
Interface Contrat que doivent respecter les implémentations
Tag Étiquette pour grouper/identifier des services

Workflow complet des services 🔄

  1. Créer une classe de service
    Dans src/Service/MonService.php
  2. Déclarer les dépendances dans le constructeur
    Avec type-hints (interfaces de préférence)
  3. Enregistrement automatique
    Grâce à App\: dans services.yaml
  4. Autowiring résout les dépendances
    Symfony injecte automatiquement les services nécessaires
  5. Utiliser le service
    Injection dans les contrôleurs ou autres services
Créer classe → Type-hint dépendances → Auto-enregistrement → Autowiring → Utilisation

Bonnes pratiques 💡

  1. Un service = Une responsabilité
    Principe de responsabilité unique (SRP)
  2. Toujours utiliser l'injection de dépendances
    Ne jamais faire new MonService()
  3. Type-hinter avec des interfaces
    MailerInterface plutôt que Mailer
  4. Services stateless (sans état)
    Ne pas stocker de données entre les requêtes
  5. Nommer clairement
    EmailSender, PriceCalculator, PdfGenerator
  6. Organiser dans src/Service/
    Sous-dossiers par domaine si nécessaire
  7. Tester les services
    Les services sont facilement testables unitairement

Erreurs courantes à éviter ⚠️

❌ Erreur 1 : Créer manuellement les services

// ❌ MAUVAIS
$mailer = new Mailer();
$logger = new Logger();

// ✅ BON
public function send(MailerInterface $mailer, LoggerInterface $logger)

❌ Erreur 2 : Service avec état (stateful)

// ❌ MAUVAIS - État persistant entre requêtes
class BadService
{
    private $user;  // ← Problème !
    
    public function setUser($user) {
        $this->user = $user;
    }
}

// ✅ BON - Stateless
class GoodService
{
    public function process(User $user) {
        // Utilise $user localement seulement
    }
}

❌ Erreur 3 : Injecter le container complet

// ❌ MAUVAIS (Service Locator anti-pattern)
public function send(ContainerInterface $container)
{
    $mailer = $container->get(MailerInterface::class);
}

// ✅ BON
public function send(MailerInterface $mailer)

Ressources pour aller plus loin 📚

Documentation Symfony - Service Container

🌐 https://symfony.com/doc/current/service_container.html

Guide complet sur les services

Dependency Injection Component

🌐 https://symfony.com/doc/current/components/dependency_injection.html

Composant DI en détail

Autowiring

🌐 https://symfony.com/doc/current/service_container/autowiring.html

Tout sur l'autowiring automatique

Service Tags

🌐 https://symfony.com/doc/current/service_container/tags.html

Référence des tags Symfony

Ce que vous maîtrisez maintenant ! 💪

Compétences acquises :

🎓 Félicitations !

Vous maîtrisez les Services et l'Injection de Dépendances

Vous savez maintenant :

✅ Créer des services réutilisables

✅ Utiliser l'autowiring automatique

✅ Organiser votre code proprement

✅ Appliquer les principes SOLID

💡 Conseil final :

Les services sont le CŒUR de Symfony. Maîtriser l'injection de dépendances et l'autowiring, c'est maîtriser Symfony ! Créez des services pour toute logique réutilisable.

Série complète Symfony terminée ! 🚀

Formation DWWM - 2026