# Symfony dependency injection: service container, autowiring en tags in 2026 > Diepgaande blik op Symfony dependency injection: de service container, autowiring, het #[Autowire]-attribuut, interfaces binden, getagde services met #[AutowireIterator], compiler passes en lazy services in Symfony 7.4 en 8.0. - Published: 2026-07-06 - Updated: 2026-07-07 - Author: SharpSkill - Tags: symfony, dependency injection, service container, autowiring, php, di - Reading time: 11 min --- Symfony dependency injection vormt de ruggengraat van elke Symfony-applicatie: de service container instantieert, configureert en verbindt objecten met elkaar, zodat klassen aangeven wat ze nodig hebben in plaats van het zelf te construeren. In Symfony 7.4 en 8.0 hebben autowiring, PHP-attributen en getagde services de expliciete configuratie tot vrijwel niets teruggebracht, terwijl de container tijdens compile time volledig opgelost en voorspelbaar blijft. Begrijpen hoe die resolutie werkt, scheidt de ontwikkelaars die tegen de container vechten van degenen die hem het werk laten doen. > **Wat is dependency injection in Symfony?** > > Dependency injection in Symfony is een patroon waarbij de service container de afhankelijkheden van een object via zijn constructor doorgeeft in plaats van het object ze zelf te laten aanmaken. Autowiring lost elk argument met een type-hint automatisch op naar een passende service, waardoor de meeste klassen helemaal geen handmatige configuratie nodig hebben. ## Hoe de Symfony service container afhankelijkheden oplost De service container is een gecompileerde PHP-klasse die weet hoe elke service in de applicatie gebouwd moet worden. Tijdens de compilatiefase van de container scant Symfony de klassedefinities, leest de constructor-signaturen en produceert een gecachte factory. Op runtime levert het opvragen van een service een lazy gebouwde, gedeelde instantie op. Omdat het verbinden op compile time gebeurt, faalt een verkeerd geconfigureerde afhankelijkheid wanneer de cache wordt gebouwd, niet wanneer een request de productie bereikt. Een service is simpelweg een klasse die een taak uitvoert en door de container wordt beheerd. De standaard `config/services.yaml` registreert alles onder `src/` als service met autowiring en autoconfiguration ingeschakeld, en daarom werkt een gewone klasse zonder extra opzet. ```yaml # 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' ``` Met die standaardwaarden vraagt een klasse haar medewerkers op via de constructor en levert de container ze aan. Geen factory, geen `new`, geen handmatige argumentenlijst. ```php // src/Notification/SlackNotifier.php 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]); } } ``` De container instantieert `SlackNotifier` één keer, injecteert de gedeelde HTTP-client en logger en hergebruikt die instantie overal waar ze wordt opgevraagd. Juist dit standaard delen maakt de container zowel snel als geheugenefficiënt. Het volledige model staat gedocumenteerd in de officiële [Symfony-gids over de service container](https://symfony.com/doc/current/service_container.html), die voortbouwt op de standaard van de [PSR-11 container-interface](https://www.php-fig.org/psr/psr-11/) die het hele PHP-ecosysteem deelt. Het delen kan worden uitgeschakeld wanneer per request een verse instantie nodig is, bijvoorbeeld een stateful builder die gegevens verzamelt. `shared: false` op een definitie zetten maakt er een factory van die bij elke injectie een nieuw object teruggeeft. In de praktijk komt dit zelden voor, omdat de meeste services stateless medewerkers zijn, maar weten dat deze standaard bestaat en hoe je hem overschrijft telt zodra een bug te herleiden is tot twee consumenten die onverwacht hetzelfde object muteren. ## Symfony autowiring: type-hints en het Autowire-attribuut Symfony-autowiring lost constructor-argumenten op aan de hand van hun typedeclaratie. Wanneer een argument een type-hint heeft met een klasse of interface die naar precies één service verwijst, injecteert de container die zonder enige configuratie. Autowiring loopt alleen vast op waarden die geen services zijn: scalars, omgevingsvariabelen en parameters. Het attribuut `#[Autowire]` dicht dat gat direct op het argument en houdt de bedrading naast de code die haar gebruikt. ```php // src/Service/PaymentGateway.php 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, ) {} } ``` Elk argument declareert zijn eigen bron: een parameter, een omgevingsvariabele of een benoemde service. Het attribuut accepteert ook een `expression:`-argument voor waarden uit de Expression Language, wat handig is wanneer een afhankelijkheid uit een berekening op runtime komt in plaats van uit een statische definitie. Deze metadata op de constructor houden betekent dat een lezer precies ziet waar elke waarde vandaan komt zonder een apart YAML-bestand te openen. De referentie voor het attribuut staat in de [documentatie over Symfony-autowiring](https://symfony.com/doc/current/service_container/autowiring.html). > **Geef interfaces een type-hint, geen concrete klassen** > > Autowiring op een concrete klasse als `S3Storage` koppelt elke consument aan die implementatie en maakt vervangen in tests pijnlijk. Geef in plaats daarvan de interface een type-hint en laat `#[AsAlias]` of `#[Autowire(service: ...)]` de concrete service bepalen. De hele container injecteren om services met de hand op te halen, ondermijnt de controles op compile time en is precies het anti-patroon dat autowiring bestaat om te verwijderen. ## Interfaces binden wanneer er meerdere implementaties bestaan Autowiring tegen een interface werkt schoon tot twee klassen die interface implementeren. Op dat punt kan de container niet raden welke te injecteren en gooit een ambiguïteitsfout op compile time. Twee attributen lossen dit op: `#[AsAlias]` markeert één implementatie als de standaard voor de interface, en `#[Autowire(service: ...)]` pint een specifieke implementatie vast op een afzonderlijk argument. ```php // src/Storage/StorageInterface.php namespace App\Storage; interface StorageInterface { public function put(string $key, string $contents): void; } ``` ```php // src/Storage/S3Storage.php 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 */ } } ``` ```php // src/Service/BackupService.php 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, ) {} } ``` Het attribuut `#[Target]` biedt een derde optie: het verwijst naar een implementatie via haar autowiring-aliasnaam, waardoor de naam van de argumentvariabele wordt losgekoppeld van de resolutie. De keuze tussen deze opties komt neer op intentie. Gebruik `#[AsAlias]` wanneer één implementatie werkelijk de standaard is in de hele codebase, en `#[Autowire(service: ...)]` of `#[Target]` wanneer een enkele consument een niet-standaard variant nodig heeft. Dit patroon duikt voortdurend op in productie-applicaties en is een geliefd onderwerp in de [interviewmodule over Symfony dependency injection](/technologies/symfony/interview-questions/dependency-injection). ## Symfony service-tags en de AutowireIterator-collector Tags zijn de manier waarop Symfony verwante services groepeert zodat een andere service ze allemaal tegelijk kan consumeren. Dit voedt de strategy-, chain-of-responsibility- en pluginpatronen die overal in het framework zelf opduiken: form type-extensions, Twig-extensions en event subscribers zijn allemaal tag-gedreven. In plaats van handmatig te taggen vertelt `#[AutoconfigureTag]` op een interface de container om elke implementerende klasse automatisch te taggen. ```php // src/Export/ExporterInterface.php 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; } ``` Individuele implementaties bepalen hun positie in de collectie met `#[AsTaggedItem]`, dat een prioriteit instelt (hoger draait eerst) en een optionele string-index voor toegang op sleutel. ```php // src/Export/CsvExporter.php 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)); } } ``` De consumerende service verzamelt elke getagde exporter in een geordende `iterable` met `#[AutowireIterator]`. Vervolgens loopt hij door de collectie en delegeert aan de eerste strategie die het gevraagde formaat ondersteunt. ```php // src/Export/ExportManager.php 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"); } } ``` Wanneer de collectie groot of duur is om te bouwen, is `#[AutowireLocator]` het lazy alternatief: het injecteert een PSR-11-container die een getagde service pas instantieert wanneer die daadwerkelijk op sleutel wordt opgehaald. Dat onderscheid tussen eager iteratie en lazy opzoeken telt voor services die verbindingen of zware state vasthouden. ## Autoconfiguration, compiler passes en lazy services Autoconfiguration is het mechanisme dat attributen als `#[AutoconfigureTag]` en `#[AsTaggedItem]` leest en ze tijdens de compilatie toepast. Het is de reden waarom het implementeren van `EventSubscriberInterface` genoeg is om een event subscriber met nul configuratie te registreren. Voor transformaties die attributen niet kunnen uitdrukken, haakt een compiler pass rechtstreeks in op de container-build en muteert definities voordat de cache wordt weggeschreven. De broncode van de `DependencyInjection`-component op [GitHub](https://github.com/symfony/dependency-injection) laat zien hoe het framework zelf intern tientallen van deze passes gebruikt. Prestatiegevoelige grafen profiteren van lazy services. Een argument markeren met `#[Autowire(lazy: true)]` stelt de instantiatie uit tot de afhankelijkheid voor het eerst wordt aangeroepen, gesteund door de native lazy objects van PHP 8.4 in plaats van een gegenereerde proxy-bibliotheek. ```php // src/Service/ReportBuilder.php 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'); } } ``` Native lazy objects kwamen met PHP 8.4 en worden uitgebreid behandeld in de gids over [Symfony 8 en de lazy objects van PHP 8.4](/blog/symfony/symfony-8-new-features-php-84-lazy-objects). De winst is een container die slank blijft: zware medewerkers zoals PDF-renderers, SDK-clients van derden of rapportgeneratoren kosten niets tot het codepad dat ze nodig heeft daadwerkelijk draait. ## Symfony DI: interviewvragen en antwoorden Een veelvoorkomende Symfony-DI-interviewvraag gaat over het verschil tussen autowiring en autoconfiguration. Autowiring lost de constructor-argumenten van een afzonderlijke service op op basis van type, terwijl autoconfiguration tags, methode-aanroepen en instellingen op een service toepast op basis van de interfaces die hij implementeert. Het zijn onafhankelijke schakelaars: een service kan autowired zijn zonder autoconfigured te zijn, en andersom. Een andere frequente vraag is waarom de container onderscheid maakt tussen publieke en private services. Services zijn standaard privaat, wat betekent dat ze niet rechtstreeks uit de container kunnen worden opgehaald met `$container->get()` en alleen kunnen worden geïnjecteerd. Private services kunnen tijdens de compilatie worden geïnlined en weggeoptimaliseerd, wat sneller is; publieke services moeten opvraagbaar blijven, dus de optimizer laat ze intact. Services met de hand uit de container halen wordt juist daarom als een anti-patroon beschouwd, omdat het deze optimalisatie ondermijnt en afhankelijkheden verbergt. Circulaire afhankelijkheden zijn een klassieke valkuil en een frequente vervolgvraag. Als service A in zijn constructor B nodig heeft en B A nodig heeft, kan de container geen van beide bouwen en meldt een circulaire referentie op compile time. De standaardoplossingen zijn de cyclus doorbreken door het gedeelde gedrag naar een derde service te halen, één kant omschakelen naar lazy injectie met `#[Autowire(lazy: true)]` zodat de afhankelijkheid pas bij de eerste aanroep wordt opgelost, of setter injectie gebruiken zodat het object bestaat voordat zijn medewerker wordt bedraad. Herkennen waarom de cyclus een design smell is, en niet alleen hoe je hem het zwijgen oplegt, is waar interviewers werkelijk naar luisteren. Interviewers peilen ook de timing van de container-resolutie. Omdat Symfony de container één keer compileert en cachet, komt een onoplosbare afhankelijkheid tijdens de cache-warmup aan het licht in plaats van op het moment van de request. Die garantie is een rechtstreeks gevolg van het compile-time-model en een van de redenen waarom grote Symfony-applicaties voorspelbaar blijven onder belasting. Voor een breder geheel van deze vragen groepeert het [Symfony-technologietraject](/technologies/symfony) ze per component en moeilijkheidsgraad. ## Conclusie - Laat de container het werk doen: registreer klassen onder `src/`, geef afhankelijkheden een type-hint in de constructor en sla handmatige YAML-bedrading volledig over. - Grijp naar `#[Autowire]` alleen voor niet-services: parameters, omgevingsvariabelen en specifieke service-id's. - Los interface-ambiguïteit op met `#[AsAlias]` voor de standaard en `#[Autowire(service: ...)]` of `#[Target]` voor expliciete keuzes. - Stuur strategy- en pluginpatronen aan met `#[AutoconfigureTag]`, de prioriteiten van `#[AsTaggedItem]` en `#[AutowireIterator]`; schakel over naar `#[AutowireLocator]` wanneer lazy opzoeken te verkiezen is boven eager iteratie. - Stel dure medewerkers uit met `#[Autowire(lazy: true)]`, dat de native lazy objects van PHP 8.4 gebruikt. - Houd services privaat en geïnjecteerd in plaats van opgehaald, zodat de compiler de graaf kan inlinen en optimaliseren. --- Source: SharpSkill (https://sharpskill.dev), tech interview preparation for your real stack. HTML version of this page: https://sharpskill.dev/nl/blog/symfony/symfony-dependency-injection-service-container-autowiring-tags