Injection de dépendances Symfony : conteneur de services, autowiring et tags en 2026
Plongée dans l'injection de dépendances Symfony : conteneur de services, autowiring, attribut #[Autowire], liaison d'interfaces, services taggés avec #[AutowireIterator], compiler passes et services paresseux dans Symfony 7.4 et 8.0.

L'injection de dépendances de Symfony est la colonne vertébrale de toute application Symfony : le conteneur de services instancie, configure et relie les objets entre eux, si bien que les classes déclarent ce dont elles ont besoin au lieu de le construire elles-mêmes. Dans Symfony 7.4 et 8.0, l'autowiring, les attributs PHP et les services taggés ont réduit la configuration explicite à presque rien, tout en gardant le conteneur entièrement résolu et prévisible au moment de la compilation. Comprendre le fonctionnement de cette résolution sépare les développeurs qui luttent contre le conteneur de ceux qui le laissent travailler.
L'injection de dépendances dans Symfony est un pattern où le conteneur de services transmet à un objet ses dépendances via son constructeur plutôt que de laisser l'objet les créer. L'autowiring associe automatiquement chaque argument typé à un service correspondant, si bien que la plupart des classes ne nécessitent aucune configuration manuelle.
Comment le conteneur de services Symfony résout les dépendances
Le conteneur de services est une classe PHP compilée qui sait construire chaque service de l'application. Pendant la phase de compilation du conteneur, Symfony analyse les définitions de classes, lit les signatures des constructeurs et produit une fabrique mise en cache. À l'exécution, demander un service renvoie une instance partagée, construite paresseusement. Comme le câblage a lieu à la compilation, une dépendance mal configurée échoue lors de la construction du cache, et non lorsqu'une requête frappe la production.
Un service est simplement une classe qui accomplit une tâche et qui est gérée par le conteneur. Le fichier config/services.yaml par défaut enregistre tout ce qui se trouve sous src/ comme service, avec l'autowiring et l'autoconfiguration activés, ce qui explique qu'une classe ordinaire fonctionne sans réglage supplémentaire.
# config/services.yaml
services:
_defaults:
autowire: true # inject arguments by type-hint
autoconfigure: true # apply tags based on implemented interfaces
App\:
resource: '../src/'
exclude:
- '../src/Entity/'
- '../src/Kernel.php'Avec ces valeurs par défaut en place, une classe réclame ses collaborateurs via son constructeur et le conteneur les fournit. Pas de fabrique, pas de new, pas de liste d'arguments à écrire à la main.
namespace App\Notification;
use Psr\Log\LoggerInterface;
use Symfony\Contracts\HttpClient\HttpClientInterface;
final class SlackNotifier
{
// The container reads each type-hint and injects a matching service
public function __construct(
private readonly HttpClientInterface $httpClient,
private readonly LoggerInterface $logger,
) {}
public function send(string $channel, string $message): void
{
$this->httpClient->request('POST', 'https://slack.example/api/post', [
'json' => ['channel' => $channel, 'text' => $message],
]);
$this->logger->info('Slack message dispatched', ['channel' => $channel]);
}
}Le conteneur instancie SlackNotifier une seule fois, y injecte le client HTTP et le logger partagés, puis réutilise cette instance partout où elle est demandée. Ce comportement partagé par défaut est ce qui rend le conteneur à la fois rapide et économe en mémoire. Le modèle complet est décrit dans le guide officiel du conteneur de services Symfony, qui s'appuie sur le standard interface de conteneur PSR-11 partagé par tout l'écosystème PHP.
Le partage peut être désactivé lorsqu'une nouvelle instance est requise à chaque requête, par exemple un builder à état qui accumule des données. Définir shared: false sur une définition la transforme en fabrique qui renvoie un nouvel objet à chaque injection. C'est rare en pratique, car la plupart des services sont des collaborateurs sans état, mais savoir que ce défaut existe et comment le surcharger compte lorsqu'un bug remonte à deux consommateurs qui mutent par erreur le même objet.
Autowiring Symfony : type hints et l'attribut Autowire
L'autowiring de Symfony résout les arguments du constructeur d'après leur déclaration de type. Quand un argument est typé avec une classe ou une interface qui correspond à exactement un service, le conteneur l'injecte sans aucune configuration. L'autowiring ne cède que pour les valeurs qui ne sont pas des services : scalaires, variables d'environnement et paramètres. L'attribut #[Autowire] comble cette lacune directement sur l'argument, en gardant le câblage au plus près du code qui l'utilise.
namespace App\Service;
use Psr\Log\LoggerInterface;
use Symfony\Component\DependencyInjection\Attribute\Autowire;
final class PaymentGateway
{
public function __construct(
// Inject a container parameter with the %...% syntax
#[Autowire('%kernel.environment%')]
private readonly string $environment,
// Inject an environment variable, resolved at runtime
#[Autowire(env: 'STRIPE_SECRET')]
private readonly string $apiKey,
// Bind a specific service when the type alone is ambiguous
#[Autowire(service: 'monolog.logger.payment')]
private readonly LoggerInterface $logger,
) {}
}Chaque argument déclare sa propre source : un paramètre, une variable d'environnement ou un service nommé. L'attribut accepte aussi un argument expression: pour les valeurs Expression Language, utile lorsqu'une dépendance provient d'un calcul à l'exécution plutôt que d'une définition statique. Garder ces métadonnées sur le constructeur permet à un lecteur de voir exactement d'où provient chaque valeur sans ouvrir un fichier YAML séparé. La référence de l'attribut se trouve dans la documentation de l'autowiring Symfony.
Faire l'autowiring d'une classe concrète comme S3Storage couple chaque consommateur à cette implémentation et rend son remplacement pénible dans les tests. Typez plutôt l'interface et laissez #[AsAlias] ou #[Autowire(service: ...)] choisir le service concret. Injecter le conteneur entier pour aller chercher des services à la main annule les vérifications à la compilation et constitue précisément l'anti-pattern que l'autowiring existe pour supprimer.
Lier des interfaces quand plusieurs implémentations coexistent
L'autowiring contre une interface fonctionne proprement jusqu'à ce que deux classes l'implémentent. À ce moment, le conteneur ne peut pas deviner laquelle injecter et lève une erreur d'ambiguïté à la compilation. Deux attributs résolvent cela : #[AsAlias] désigne une implémentation comme celle par défaut pour l'interface, et #[Autowire(service: ...)] épingle une implémentation précise sur un argument donné.
namespace App\Storage;
interface StorageInterface
{
public function put(string $key, string $contents): void;
}namespace App\Storage;
use Symfony\Component\DependencyInjection\Attribute\AsAlias;
// Becomes the default service returned for StorageInterface
#[AsAlias(StorageInterface::class)]
final class S3Storage implements StorageInterface
{
public function put(string $key, string $contents): void { /* upload to S3 */ }
}namespace App\Service;
use App\Storage\LocalStorage;
use App\Storage\StorageInterface;
use Symfony\Component\DependencyInjection\Attribute\Autowire;
final class BackupService
{
public function __construct(
// Resolves to the #[AsAlias] default (S3Storage)
private readonly StorageInterface $primary,
// Pins the concrete implementation explicitly by service id
#[Autowire(service: LocalStorage::class)]
private readonly StorageInterface $fallback,
) {}
}L'attribut #[Target] offre une troisième option : il référence une implémentation par son nom d'alias d'autowiring, découplant ainsi le nom de la variable d'argument de la résolution. Le choix entre ces mécanismes tient à l'intention. Utilisez #[AsAlias] quand une implémentation est réellement celle par défaut dans toute la base de code, et #[Autowire(service: ...)] ou #[Target] quand un seul consommateur a besoin d'une variante non standard. Ce pattern revient sans cesse dans les applications de production, et c'est un sujet de prédilection du module d'entretien sur l'injection de dépendances Symfony.
Prêt à réussir tes entretiens Symfony ?
Entraîne-toi avec nos simulateurs interactifs, fiches express et tests techniques.
Tags de services Symfony et le collecteur AutowireIterator
Les tags sont la façon dont Symfony regroupe des services liés pour qu'un autre service les consomme tous d'un coup. Cela alimente les patterns strategy, chaîne de responsabilité et plugin qui traversent le framework lui-même : les extensions de type de formulaire, les extensions Twig et les event subscribers reposent tous sur les tags. Plutôt que de tagguer manuellement, #[AutoconfigureTag] posé sur une interface indique au conteneur de tagguer automatiquement chaque classe qui l'implémente.
namespace App\Export;
use Symfony\Component\DependencyInjection\Attribute\AutoconfigureTag;
// Every class implementing this interface is tagged automatically
#[AutoconfigureTag('app.exporter')]
interface ExporterInterface
{
public function supports(string $format): bool;
public function export(array $rows): string;
}Chaque implémentation contrôle sa position dans la collection avec #[AsTaggedItem], qui fixe une priorité (la plus élevée s'exécute en premier) et un index de chaîne optionnel pour un accès par clé.
namespace App\Export;
use Symfony\Component\DependencyInjection\Attribute\AsTaggedItem;
// Higher priority services appear earlier in the injected iterable
#[AsTaggedItem(priority: 10)]
final class CsvExporter implements ExporterInterface
{
public function supports(string $format): bool
{
return $format === 'csv';
}
public function export(array $rows): string
{
return implode("\n", array_map(fn ($r) => implode(',', $r), $rows));
}
}Le service consommateur rassemble chaque exporter taggé dans un iterable ordonné à l'aide de #[AutowireIterator]. Il parcourt ensuite la collection et délègue à la première stratégie qui prend en charge le format demandé.
namespace App\Export;
use Symfony\Component\DependencyInjection\Attribute\AutowireIterator;
final class ExportManager
{
// Collects all services tagged 'app.exporter', ordered by priority
public function __construct(
#[AutowireIterator('app.exporter')]
private readonly iterable $exporters,
) {}
public function export(string $format, array $rows): string
{
foreach ($this->exporters as $exporter) {
if ($exporter->supports($format)) {
return $exporter->export($rows);
}
}
throw new \InvalidArgumentException("No exporter for format: $format");
}
}Quand la collection est volumineuse ou coûteuse à construire, #[AutowireLocator] est l'alternative paresseuse : il injecte un conteneur PSR-11 qui n'instancie un service taggé que lorsqu'il est réellement récupéré par sa clé. Cette distinction entre itération avide et récupération paresseuse compte pour les services qui détiennent des connexions ou un état lourd.
Autoconfiguration, compiler passes et services paresseux
L'autoconfiguration est le mécanisme qui lit des attributs comme #[AutoconfigureTag] et #[AsTaggedItem] et les applique pendant la compilation. C'est pourquoi implémenter EventSubscriberInterface suffit à enregistrer un event subscriber sans aucune configuration. Pour les transformations que les attributs ne peuvent pas exprimer, un compiler pass s'accroche directement à la construction du conteneur et mute les définitions avant l'écriture du cache. Le code source du composant DependencyInjection sur GitHub montre comment le framework lui-même utilise en interne des dizaines de ces passes.
Les graphes sensibles à la performance profitent des services paresseux. Marquer un argument avec #[Autowire(lazy: true)] diffère l'instanciation jusqu'au premier appel de la dépendance, en s'appuyant sur les objets paresseux natifs de PHP 8.4 plutôt que sur une bibliothèque de proxys générés.
namespace App\Service;
use App\Pdf\PdfRenderer;
use Symfony\Component\DependencyInjection\Attribute\Autowire;
final class ReportBuilder
{
public function __construct(
// PdfRenderer is only instantiated on first actual use
#[Autowire(lazy: true)]
private readonly PdfRenderer $renderer,
) {}
public function buildMonthly(): string
{
// If no method touches $this->renderer, it is never constructed
return $this->renderer->render('monthly-report');
}
}Les objets paresseux natifs sont arrivés avec PHP 8.4 et sont traités en détail dans le guide sur Symfony 8 et les objets paresseux de PHP 8.4. Le gain est un conteneur qui reste léger : des collaborateurs lourds comme les générateurs de PDF, les clients de SDK tiers ou les générateurs de rapports ne coûtent rien tant que le chemin de code qui les réclame ne s'exécute pas.
Questions et réponses d'entretien sur la DI Symfony
Une question d'entretien courante sur la DI Symfony demande la différence entre autowiring et autoconfiguration. L'autowiring résout les arguments du constructeur d'un service donné par leur type, tandis que l'autoconfiguration applique tags, appels de méthode et réglages à un service en fonction des interfaces qu'il implémente. Ce sont deux interrupteurs indépendants : un service peut être autowiré sans être autoconfiguré, et inversement.
Autre question fréquente : pourquoi le conteneur distingue-t-il les services publics et privés. Les services sont privés par défaut, ce qui signifie qu'ils ne peuvent pas être récupérés directement depuis le conteneur avec $container->get() et ne peuvent qu'être injectés. Les services privés peuvent être inlinés et éliminés par optimisation pendant la compilation, ce qui est plus rapide ; les services publics doivent rester récupérables, si bien que l'optimiseur les laisse intacts. Récupérer des services manuellement depuis le conteneur est considéré comme un anti-pattern précisément parce que cela annule cette optimisation et masque les dépendances.
Les dépendances circulaires sont un piège classique et une relance fréquente. Si le service A a besoin de B dans son constructeur et que B a besoin de A, le conteneur ne peut construire ni l'un ni l'autre et signale une référence circulaire à la compilation. Les corrections standards consistent à briser le cycle en extrayant le comportement partagé dans un troisième service, à basculer un côté vers l'injection paresseuse avec #[Autowire(lazy: true)] afin que la dépendance ne soit résolue qu'au premier appel, ou à recourir à l'injection par setter pour que l'objet existe avant que son collaborateur soit câblé. Reconnaître pourquoi le cycle est un défaut de conception, et pas seulement comment le faire taire, est ce que les recruteurs écoutent vraiment.
Les recruteurs sondent aussi le moment de la résolution du conteneur. Comme Symfony compile le conteneur une fois et le met en cache, une dépendance irrésolvable fait surface au réchauffement du cache plutôt qu'au moment de la requête. Cette garantie est une conséquence directe du modèle à la compilation et l'une des raisons pour lesquelles les grosses applications Symfony restent prévisibles sous charge. Pour un ensemble plus large de ces questions, le parcours de la technologie Symfony les regroupe par composant et par difficulté.
Conclusion
- Laissez le conteneur travailler : enregistrez les classes sous
src/, typez les dépendances dans le constructeur et évitez tout câblage YAML manuel. - Recourez à
#[Autowire]uniquement pour les non-services, les paramètres, les variables d'environnement et les identifiants de service précis. - Résolvez l'ambiguïté d'interface avec
#[AsAlias]pour le défaut et#[Autowire(service: ...)]ou#[Target]pour les choix explicites. - Pilotez les patterns strategy et plugin avec
#[AutoconfigureTag], les priorités#[AsTaggedItem]et#[AutowireIterator]; passez à#[AutowireLocator]quand la récupération paresseuse vaut mieux que l'itération avide. - Différez les collaborateurs coûteux avec
#[Autowire(lazy: true)], qui utilise les objets paresseux natifs de PHP 8.4. - Gardez les services privés et injectés plutôt que récupérés, pour que le compilateur puisse inliner et optimiser le graphe.
Passe à la pratique !
Teste tes connaissances avec nos simulateurs d'entretien et tests techniques.
Tags
Partager
Articles similaires

Doctrine ORM : Maîtriser les relations en Symfony
Guide complet des relations Doctrine ORM dans Symfony. OneToMany, ManyToMany, stratégies de chargement et optimisation des performances avec exemples pratiques.

Questions d'entretien Symfony : Top 25 en 2026
Les 25 questions d'entretien Symfony les plus posées. Architecture, Doctrine ORM, services, sécurité, formulaires et tests avec réponses détaillées et exemples de code.

Symfony 7 : API Platform et bonnes pratiques
Guide complet pour créer des APIs REST professionnelles avec Symfony 7 et API Platform 4. State Providers, Processors, validation et sérialisation expliqués.