Chapitre 5.1 — Le conteneur & l’autowiring
Partie 5 : Services & DI — Chapitre 1/5 Version de référence : Symfony 7.4.
⏱️ TL;DR
- Un service est un objet qui fait un travail (envoyer un e-mail, créer une réservation) et vit dans le conteneur de services.
- Injection de dépendances (DI) : une classe déclare ses dépendances dans son constructeur ; le conteneur les lui fournit. On ne fait jamais
newd’un service.- Autowiring : Symfony lit les types des arguments du constructeur et injecte automatiquement le bon service. Zéro configuration dans le cas courant.
- Injectez des interfaces, pas des classes concrètes : code découplé, testable, remplaçable.
- Par défaut,
config/services.yamlauto-enregistre toutsrc/comme services, avecautowire: trueetautoconfigure: true.- Un service est un singleton (une instance partagée par requête).
- Diagnostic :
debug:autowiring(quels types sont injectables),debug:container(tous les services).- Pour un dev Next.js : c’est NestJS —
@Injectable()+ injection par constructeur — mais l’autowiring évite même de déclarer les providers.
🎯 Objectifs
- Comprendre le rôle du conteneur et le principe de la DI.
- Créer un service et le faire autowirer dans un contrôleur.
- Injecter une interface avec une seule implémentation.
- Diagnostiquer l’autowiring en ligne de commande.
1. Le problème que la DI résout
Sans injection, une classe crée ses dépendances — et se couple à elles :
// ❌ couplage fort : impossible à tester, à remplacer
final class BookingCreator
{
public function create(CreateBookingInput $input): Booking
{
$mailer = new SmtpMailer('smtp://localhost'); // dépendance créée en dur
$logger = new FileLogger('/var/log/app.log'); // idem
// ...
}
}Impossible de tester BookingCreator sans un vrai serveur SMTP, ni de remplacer le mailer. La DI inverse ça : la classe reçoit ses dépendances.
// ✅ dépendances injectées : testable, découplé
final class BookingCreator
{
public function __construct(
private readonly MailerInterface $mailer,
private readonly LoggerInterface $logger,
) {}
public function create(CreateBookingInput $input): Booking
{
// $this->mailer, $this->logger sont fournis « de l'extérieur »
}
}En test, on injecte un faux mailer ; en prod, le vrai. La classe ne change pas. C’est le fondement de tout code Symfony maintenable.
💡 Pour un dev Next.js : si vous avez fait du NestJS, c’est identique :
constructor(private mailer: MailerService)+@Injectable(). Si vous venez d’Express, c’est le passage du « j’importe et j’instancie mes utilitaires dans chaque fichier » au « je déclare mes besoins, un conteneur me les donne ». Le gain : testabilité et découplage.
2. Le conteneur de services
Le conteneur est un objet qui sait construire tous les services de l’application et résoudre leurs dépendances. Quand un contrôleur demande un BookingCreator, le conteneur :
- regarde de quoi
BookingCreatora besoin (son constructeur :MailerInterface,LoggerInterface) ; - construit (ou récupère) ces dépendances (elles-mêmes ont peut-être des dépendances…) ;
- instancie
BookingCreatoravec elles ; - le réutilise (singleton) pour les demandes suivantes.
Ce graphe peut être profond (un service dépend d’un service qui dépend de trois autres…). Le conteneur le résout entièrement et de façon paresseuse (les services ne sont construits que lorsqu’on en a besoin). En prod, ce câblage est compilé une fois (dans var/cache/) → aucune surcharge à l’exécution.
3. L’autowiring : le câblage sans effort
L’autowiring est la fonctionnalité qui lit les types des arguments et devine le service à injecter. Vous n’avez, dans l’immense majorité des cas, rien à configurer : il suffit de typer vos dépendances.
// src/Service/SlotAvailabilityChecker.php
declare(strict_types=1);
namespace App\Service;
use App\Repository\BookingRepository;
use Psr\Log\LoggerInterface;
final class SlotAvailabilityChecker
{
public function __construct(
private readonly BookingRepository $bookings, // autowired
private readonly LoggerInterface $logger, // autowired
) {}
public function isAvailable(int $slotId, int $seats): bool
{
// ...
return true;
}
}Ce service est immédiatement injectable dans un contrôleur ou un autre service, sans aucune ligne de config :
#[Route('/api/slots/{id}/availability', methods: ['GET'])]
public function availability(int $id, SlotAvailabilityChecker $checker): JsonResponse
{
return $this->json(['available' => $checker->isAvailable($id, 1)]);
}C’est ça, la « magie » — qui n’en est pas une : Symfony lit les type-hints et relie les points.
4. La config qui rend tout ça automatique
Comment SlotAvailabilityChecker est-il devenu un service sans qu’on le déclare ? Grâce à config/services.yaml, généré à la création du projet :
# config/services.yaml
services:
_defaults:
autowire: true # injecte automatiquement les dépendances par type
autoconfigure: true # tague automatiquement (commandes, subscribers…)
# Enregistre TOUTES les classes de src/ comme services
App\:
resource: '../src/'
exclude:
- '../src/DependencyInjection/'
- '../src/Entity/'
- '../src/Kernel.php'Traduction : toute classe de src/ (sauf les entités et quelques exclusions) est automatiquement un service, autowiré et autoconfiguré. Vous créez une classe dans src/Service/, elle est instantanément injectable. Les entités sont exclues (ce sont des données, pas des services — Partie 6).
⚡ Depuis Symfony 5 : cette config par défaut est stable, mais l’usage des attributs (
#[Autowire],#[AsEventListener],#[AutoconfigureTag]) pour les cas particuliers a largement remplacé le YAML verbeux. On configure de moins en moins dansservices.yaml; on annote directement les classes.
5. La règle d’or : injecter des interfaces
Quand vous dépendez d’un comportement, dépendez de son interface, pas de la classe concrète :
// ✅ dépend du contrat, pas de l'implémentation
public function __construct(private readonly MailerInterface $mailer) {}
// ❌ dépend d'une implémentation précise → couplage
public function __construct(private readonly SmtpMailer $mailer) {}Pourquoi ? Parce que l’implémentation peut changer (SMTP → API → faux mailer en test) sans toucher votre code. Symfony autowire une interface s’il n’existe qu’une seule implémentation (il fait le lien automatiquement). S’il y en a plusieurs, on précise laquelle (chapitre 5.2).
⚠️ Piège : « ça marche avec la classe concrète, pourquoi s’embêter avec l’interface ? » — parce que le jour où vous voulez tester (injecter un double) ou remplacer l’implémentation, le couplage à la classe concrète vous bloque. Prendre l’habitude d’injecter des interfaces coûte peu et rapporte gros. C’est le réflexe de découplage de Symfony.
6. Les services sont des singletons
Par défaut, le conteneur crée une seule instance de chaque service et la partage partout où elle est injectée, pour toute la durée de la requête. C’est ce qu’on veut presque toujours (un service est sans état propre ; il opère sur les données qu’on lui passe).
⚠️ Piège : ne stockez pas d’état par requête dans une propriété de service (ex. « l’utilisateur courant » dans une propriété que vous modifiez). Comme le service est partagé, cet état « fuit » entre usages. Les services doivent être sans état : ils reçoivent les données en argument de méthode, pas en propriété mutable. (En mode worker FrankenPHP, cette discipline est encore plus critique.)
7. Diagnostiquer l’injection
# Quels types puis-je injecter (autowiring) ?
php bin/console debug:autowiring
php bin/console debug:autowiring mailer # filtrer
# Tous les services du conteneur (et leur classe)
php bin/console debug:container
php bin/console debug:container BookingCreator # détail d'un servicedebug:autowiring liste tous les types injectables (interfaces et classes) et l’implémentation qui sera fournie — indispensable quand une injection « ne trouve pas » le bon service.
💡 Pour un dev Next.js :
debug:autowiringrépond à « quel provider Nest injectera-t-il pour ce type ? », mais sans que vous ayez à déclarer un module. C’est l’un des grands conforts de Symfony : la découverte + le câblage sont automatiques, et outillés.
✏️ Exercices
Exercice 1. Créez un service PriceCalculator qui dépend d’un TaxRateProvider (interface) et expose total(int $amountCents): int. Montrez comment il serait injecté dans un contrôleur.
✅ Solution
// src/Service/PriceCalculator.php
final class PriceCalculator
{
public function __construct(private readonly TaxRateProvider $taxRates) {}
public function total(int $amountCents): int
{
return (int) round($amountCents * (1 + $this->taxRates->current()));
}
}
// dans un contrôleur : autowiring, aucune config
public function quote(int $amount, PriceCalculator $calc): JsonResponse
{
return $this->json(['total' => $calc->total($amount)]);
}PriceCalculator est auto-enregistré (dans src/), TaxRateProvider est autowiré s’il n’a qu’une implémentation.
Exercice 2. Pourquoi injecter MailerInterface plutôt que SmtpMailer ?
✅ Solution
Pour découpler votre code de l’implémentation : vous dépendez du contrat (envoyer un e-mail), pas du comment (SMTP). On peut ainsi changer l’implémentation (SMTP → API tierce → faux mailer en test) sans modifier votre classe, et tester en injectant un double. C’est la clé de la testabilité et du remplacement.
Exercice 3. Un collègue stocke $this->currentUser dans une propriété de service qu’il modifie à chaque appel. Quel bug guette, et quelle est la bonne pratique ?
✅ Solution
Le service est un singleton partagé : stocker un état par requête/appel dans une propriété fait fuiter cet état entre les usages (et c’est catastrophique en mode worker où l’instance survit entre requêtes). Bonne pratique : les services sont sans état — passez User $user en argument de méthode plutôt que de le stocker en propriété. L’état vit dans les paramètres, pas dans le service.
🧠 Quiz de révision
1. Qu’est-ce qu’un service et où vit-il ?
Un objet qui fait un travail (mailer, créateur de réservation…), enregistré dans le conteneur de services qui le construit et le fournit à la demande.
2. Qu’est-ce que l’autowiring ?
Le mécanisme qui lit les types des arguments du constructeur et injecte automatiquement le bon service, sans configuration manuelle.
3. Pourquoi injecter des interfaces plutôt que des classes concrètes ?
Pour le découplage et la testabilité : on dépend du contrat, on peut changer l’implémentation ou injecter un double sans modifier le code.
4. Un service est-il recréé à chaque injection ?
Non : c’est un singleton — une seule instance partagée (par requête). D’où l’importance de garder les services sans état.
5. Quelle commande liste les types injectables ?
php bin/console debug:autowiring (et debug:container pour tous les services).
Prochain chapitre : 5.2 — Config des services, paramètres, environnements — services.yaml, paramètres, #[Autowire], et la config par environnement.