Le cœur de Symfony
DI • Autowiring • Service Container • Configuration
Formation DWWM - 2026
Objets réutilisables et partagés
Service = Objet PHP qui effectue une tâche spécifique et qui peut être réutilisé partout dans l'application.
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.
| 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) |
Une seule instance partagée
// Même instance partout
$logger = $container->get(Logger::class);
$logger2 = $container->get(Logger::class);
// $logger === $logger2 → TRUE
Nouvelle instance à chaque fois
new// Nouvelles instances
$user1 = new User();
$user2 = new User();
// $user1 === $user2 → FALSE
💡 Règle simple :
Dependency Injection (DI)
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
}
}
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
}
}
Sans injection (mauvais) :
Avec injection (bon) :
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.
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.
class ArticleService
{
private ?LoggerInterface $logger = null;
public function setLogger(LoggerInterface $logger): void
{
$this->logger = $logger;
}
}
⚠️ Attention : Dépendance optionnelle, risque de null.
Injection automatique par type
Autowiring = Symfony devine automatiquement quel service injecter en se basant sur le TYPE du paramètre.
L'autowiring = un majordome qui devine tes besoins :
Symfony regarde le type-hint et injecte automatiquement le bon service !
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 :
LoggerInterface $logger// Symfony analyse les types
public function send(
LoggerInterface $logger, // Type = LoggerInterface
MailerInterface $mailer, // Type = MailerInterface
string $message // Type = string → IGNORÉ (pas un service)
)
| 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/ !
Le registre central des services
Service Container = Grosse boîte qui contient TOUS les services de l'application et sait comment les créer.
Le Service Container = un entrepôt Amazon ultra-moderne :
Tu demandes "LoggerInterface", le robot va chercher le bon service et te le donne !
public function send(LoggerInterface $logger)
{
// Symfony injecte automatiquement
$logger->info('Message');
}
class ArticleController extends AbstractController
{
public function index(): Response
{
// ⚠️ Déprécié, utilisez l'autowiring à la place
$logger = $this->container->get('logger');
$logger->info('Message');
}
}
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.
Configurer les services
Fichier de configuration principal des services : config/services.yaml
# 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'
| 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 | - |
💡 Services privés vs publics :
$container->get() → À éviterServices privés = meilleure performance et code plus propre.
App\:
resource: '../src/'
exclude:
- '../src/DependencyInjection/'
- '../src/Entity/'
- '../src/Kernel.php'
✅ Ce que ça fait :
src/_defaults à tous ces services// 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)
Services personnalisés
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 !
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
]);
}
}
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 !
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);
}
}
services:
App\Service\EmailService:
arguments:
$senderEmail: 'noreply@example.com'
$senderName: 'Mon Application'
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%'
Programmation orientée interface
Une interface = une prise électrique standardisée :
process()Tu peux changer d'appareil sans changer la prise !
✅ Avantages des interfaces :
namespace App\Service\Payment;
interface PaymentProcessorInterface
{
public function process(float $amount, string $currency): array;
public function refund(string $transactionId): bool;
}
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;
}
}
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;
}
}
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)%'
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 !
Grouper et identifier des services
Tag = Étiquette qu'on met sur un service pour le marquer comme faisant partie d'un groupe.
Un tag = un badge de couleur à un événement :
Symfony regarde les badges pour savoir comment traiter chaque personne (service) !
| 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 !
namespace App\Service\Notification;
interface NotificationChannelInterface
{
public function send(string $to, string $message): void;
public function getName(): string;
}
// 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';
}
}
services:
# Tagger automatiquement toutes les classes qui implémentent NotificationChannelInterface
_instanceof:
App\Service\Notification\NotificationChannelInterface:
tags: ['app.notification_channel']
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);
}
}
}
services:
App\Service\NotificationManager:
arguments:
$channels: !tagged_iterator app.notification_channel
Commandes utiles
php bin/console debug:container
php bin/console debug:container logger
php bin/console debug:container mailer
php bin/console debug:container doctrine
php bin/console debug:container Psr\Log\LoggerInterface --show-arguments
php bin/console debug:container --tag=kernel.event_listener
php bin/console debug:autowiring
# Chercher un type spécifique
php bin/console debug:autowiring Mailer
php bin/console lint:container
php bin/console debug:container logger
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
---------------- --------------------------------------------
| 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 |
src/Service/MonService.phpApp\: dans services.yamlnew MonService()MailerInterface plutôt que MailerEmailSender, PriceCalculator, PdfGenerator// ❌ MAUVAIS
$mailer = new Mailer();
$logger = new Logger();
// ✅ BON
public function send(MailerInterface $mailer, LoggerInterface $logger)
// ❌ 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
}
}
// ❌ MAUVAIS (Service Locator anti-pattern)
public function send(ContainerInterface $container)
{
$mailer = $container->get(MailerInterface::class);
}
// ✅ BON
public function send(MailerInterface $mailer)
🌐 https://symfony.com/doc/current/service_container.html
Guide complet sur les services
🌐 https://symfony.com/doc/current/components/dependency_injection.html
Composant DI en détail
🌐 https://symfony.com/doc/current/service_container/autowiring.html
Tout sur l'autowiring automatique
🌐 https://symfony.com/doc/current/service_container/tags.html
Référence des tags Symfony
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