Symfony Dependency Injection: Service-Container, Autowiring und Tags in 2026
Tiefgehender Blick auf Symfony Dependency Injection: Service-Container, Autowiring, das Attribut #[Autowire], Interface-Binding, getaggte Services mit #[AutowireIterator], Compiler-Pässe und Lazy Services in Symfony 7.4 und 8.0.

Symfony Dependency Injection ist das Rückgrat jeder Symfony-Anwendung: Der Service-Container instanziiert, konfiguriert und verdrahtet Objekte miteinander, sodass Klassen deklarieren, was sie benötigen, statt es selbst zu konstruieren. In Symfony 7.4 und 8.0 haben Autowiring, PHP-Attribute und getaggte Services die explizite Konfiguration auf nahezu nichts reduziert und halten den Container gleichzeitig zur Kompilierzeit vollständig aufgelöst und vorhersehbar. Wer versteht, wie diese Auflösung funktioniert, gehört zu den Entwicklern, die den Container arbeiten lassen, statt gegen ihn zu kämpfen.
Dependency Injection in Symfony ist ein Muster, bei dem der Service-Container die Abhängigkeiten eines Objekts über dessen Konstruktor übergibt, statt das Objekt sie selbst erzeugen zu lassen. Autowiring löst jedes per Type-Hint deklarierte Argument automatisch auf einen passenden Service auf, sodass die meisten Klassen überhaupt keine manuelle Konfiguration benötigen.
Wie der Symfony Service-Container Abhängigkeiten auflöst
Der Service-Container ist eine kompilierte PHP-Klasse, die weiß, wie sie jeden Service der Anwendung baut. Während der Container-Kompilierungsphase durchsucht Symfony die Klassendefinitionen, liest die Konstruktor-Signaturen und erzeugt eine gecachte Factory. Zur Laufzeit liefert die Anforderung eines Service eine lazy gebaute, geteilte Instanz. Weil die Verdrahtung zur Kompilierzeit geschieht, scheitert eine falsch konfigurierte Abhängigkeit beim Bau des Caches und nicht erst, wenn ein Request die Produktion erreicht.
Ein Service ist schlicht eine Klasse, die eine Aufgabe erfüllt und vom Container verwaltet wird. Die Standard-Datei config/services.yaml registriert alles unter src/ als Service mit aktiviertem Autowiring und Autoconfiguration, weshalb eine gewöhnliche Klasse ohne zusätzliche Einrichtung funktioniert.
# 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'Mit diesen Standardwerten fordert eine Klasse ihre Mitarbeiter über den Konstruktor an, und der Container liefert sie. Keine Factory, kein new, keine manuelle Argumentliste.
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]);
}
}Der Container instanziiert SlackNotifier einmal, injiziert den geteilten HTTP-Client und den Logger und verwendet diese Instanz überall wieder, wo sie angefordert wird. Genau dieses standardmäßige Teilen macht den Container zugleich schnell und speichereffizient. Das vollständige Modell ist im offiziellen Symfony-Leitfaden zum Service-Container dokumentiert, der auf dem im gesamten PHP-Ökosystem geteilten Standard der PSR-11-Container-Schnittstelle aufbaut.
Das Teilen lässt sich abschalten, wenn pro Anforderung eine frische Instanz benötigt wird, etwa ein zustandsbehafteter Builder, der Daten ansammelt. Setzt man shared: false auf einer Definition, wird daraus eine Factory, die bei jeder Injektion ein neues Objekt zurückgibt. In der Praxis ist das selten, weil die meisten Services zustandslose Mitarbeiter sind, doch zu wissen, dass diese Voreinstellung existiert und wie man sie überschreibt, zählt, sobald sich ein Fehler auf zwei Konsumenten zurückführen lässt, die unerwartet dasselbe Objekt verändern.
Symfony Autowiring: Type-Hints und das Autowire-Attribut
Symfony-Autowiring löst Konstruktor-Argumente anhand ihrer Typdeklaration auf. Ist ein Argument mit einer Klasse oder einem Interface typisiert, das auf genau einen Service verweist, injiziert der Container ihn ohne jede Konfiguration. Autowiring versagt nur bei Werten, die keine Services sind: Skalare, Umgebungsvariablen und Parameter. Das Attribut #[Autowire] schließt diese Lücke direkt am Argument und hält die Verdrahtung neben dem Code, der sie nutzt.
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,
) {}
}Jedes Argument deklariert seine eigene Quelle: einen Parameter, eine Umgebungsvariable oder einen benannten Service. Das Attribut akzeptiert außerdem ein expression:-Argument für Werte der Expression Language, was nützlich ist, wenn eine Abhängigkeit aus einer Laufzeitberechnung statt aus einer statischen Definition stammt. Diese Metadaten am Konstruktor zu halten bedeutet, dass ein Leser genau sieht, woher jeder Wert stammt, ohne eine separate YAML-Datei zu öffnen. Die Referenz zum Attribut findet sich in der Dokumentation zum Symfony-Autowiring.
Eine konkrete Klasse wie S3Storage per Autowiring einzubinden, koppelt jeden Konsumenten an diese Implementierung und macht den Austausch in Tests mühsam. Besser typisiert man das Interface und überlässt #[AsAlias] oder #[Autowire(service: ...)] die Entscheidung über den konkreten Service. Den gesamten Container zu injizieren, um Services von Hand herauszuholen, hebelt die Prüfungen zur Kompilierzeit aus und ist genau das Anti-Pattern, das Autowiring beseitigen soll.
Interfaces binden, wenn mehrere Implementierungen existieren
Autowiring gegen ein Interface funktioniert sauber, bis zwei Klassen es implementieren. Ab diesem Punkt kann der Container nicht erraten, welche er injizieren soll, und wirft zur Kompilierzeit einen Mehrdeutigkeitsfehler. Zwei Attribute lösen das: #[AsAlias] markiert eine Implementierung als Standard für das Interface, und #[Autowire(service: ...)] heftet eine bestimmte Implementierung an ein einzelnes Argument.
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,
) {}
}Das Attribut #[Target] bietet eine dritte Möglichkeit: Es referenziert eine Implementierung über deren Autowiring-Alias-Namen und entkoppelt so den Namen der Argumentvariable von der Auflösung. Die Wahl zwischen diesen Ansätzen richtet sich nach der Absicht. #[AsAlias] passt, wenn eine Implementierung im gesamten Code wirklich der Standard ist, und #[Autowire(service: ...)] oder #[Target], wenn ein einzelner Konsument eine abweichende Variante benötigt. Dieses Muster taucht in produktiven Anwendungen ständig auf und ist ein beliebtes Thema im Interview-Modul zu Symfony Dependency Injection.
Bereit für deine Symfony-Interviews?
Übe mit unseren interaktiven Simulatoren, Flashcards und technischen Tests.
Symfony Service-Tags und der AutowireIterator-Collector
Tags sind die Art und Weise, wie Symfony verwandte Services gruppiert, damit ein anderer Service sie alle auf einmal verarbeiten kann. Das treibt die Strategy-, Chain-of-Responsibility- und Plugin-Muster an, die im Framework selbst überall auftauchen: Form-Type-Extensions, Twig-Extensions und Event-Subscriber sind allesamt Tag-gesteuert. Statt manuell zu taggen, weist #[AutoconfigureTag] auf einem Interface den Container an, jede implementierende Klasse automatisch zu taggen.
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;
}Einzelne Implementierungen steuern ihre Position in der Sammlung mit #[AsTaggedItem], das eine Priorität setzt (höhere läuft zuerst) sowie einen optionalen String-Index für den zugriffsschlüsselbasierten Zugang.
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));
}
}Der konsumierende Service sammelt jeden getaggten Exporter mit #[AutowireIterator] in ein geordnetes iterable. Anschließend durchläuft er die Sammlung und delegiert an die erste Strategie, die das angeforderte Format unterstützt.
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");
}
}Ist die Sammlung groß oder teuer im Aufbau, ist #[AutowireLocator] die lazy Alternative: Es injiziert einen PSR-11-Container, der einen getaggten Service erst dann instanziiert, wenn er tatsächlich per Schlüssel abgerufen wird. Dieser Unterschied zwischen eifriger Iteration und lazy Nachschlagen zählt bei Services, die Verbindungen oder schweren Zustand halten.
Autoconfiguration, Compiler-Pässe und Lazy Services
Autoconfiguration ist der Mechanismus, der Attribute wie #[AutoconfigureTag] und #[AsTaggedItem] liest und sie während der Kompilierung anwendet. Deshalb genügt es, EventSubscriberInterface zu implementieren, um einen Event-Subscriber ohne Konfiguration zu registrieren. Für Transformationen, die Attribute nicht ausdrücken können, klinkt sich ein Compiler-Pass direkt in den Container-Bau ein und verändert Definitionen, bevor der Cache geschrieben wird. Der Quellcode der DependencyInjection-Komponente auf GitHub zeigt, wie das Framework selbst intern Dutzende dieser Pässe einsetzt.
Performance-kritische Graphen profitieren von Lazy Services. Markiert man ein Argument mit #[Autowire(lazy: true)], verschiebt sich die Instanziierung, bis die Abhängigkeit erstmals aufgerufen wird, gestützt auf die nativen Lazy Objects von PHP 8.4 statt auf eine generierte Proxy-Bibliothek.
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 kamen mit PHP 8.4 und werden ausführlich im Leitfaden zu Symfony 8 und den Lazy Objects von PHP 8.4 behandelt. Der Gewinn ist ein Container, der schlank bleibt: schwere Mitarbeiter wie PDF-Renderer, Third-Party-SDK-Clients oder Report-Generatoren kosten nichts, bis der Code-Pfad, der sie benötigt, tatsächlich läuft.
Symfony DI: Interview-Fragen und Antworten
Eine häufige Symfony-DI-Interviewfrage zielt auf den Unterschied zwischen Autowiring und Autoconfiguration. Autowiring löst die Konstruktor-Argumente eines einzelnen Service anhand des Typs auf, während Autoconfiguration Tags, Methodenaufrufe und Einstellungen auf einen Service anwendet, basierend auf den Interfaces, die er implementiert. Es sind voneinander unabhängige Schalter: Ein Service kann autowired werden, ohne autoconfigured zu sein, und umgekehrt.
Eine weitere häufige Frage lautet, warum der Container zwischen öffentlichen und privaten Services unterscheidet. Services sind standardmäßig privat, das heißt, sie lassen sich nicht direkt mit $container->get() aus dem Container holen und können nur injiziert werden. Private Services lassen sich während der Kompilierung inlinen und wegoptimieren, was schneller ist; öffentliche Services müssen abrufbar bleiben, weshalb der Optimierer sie unangetastet lässt. Services von Hand aus dem Container zu holen, gilt genau deshalb als Anti-Pattern, weil es diese Optimierung aushebelt und Abhängigkeiten verbirgt.
Zirkuläre Abhängigkeiten sind eine klassische Falle und eine häufige Folgefrage. Braucht Service A im Konstruktor B und B wiederum A, kann der Container keinen von beiden bauen und meldet zur Kompilierzeit eine zirkuläre Referenz. Die üblichen Lösungen bestehen darin, den Zyklus durch Auslagern des gemeinsamen Verhaltens in einen dritten Service zu durchbrechen, eine Seite mit #[Autowire(lazy: true)] auf Lazy-Injection umzustellen, sodass die Abhängigkeit erst beim ersten Aufruf aufgelöst wird, oder Setter-Injection zu verwenden, damit das Objekt existiert, bevor sein Mitarbeiter verdrahtet wird. Zu erkennen, warum der Zyklus ein Design-Geruch ist und nicht bloß, wie man ihn zum Schweigen bringt, ist das, worauf Interviewer tatsächlich hören.
Interviewer hinterfragen auch das Timing der Container-Auflösung. Weil Symfony den Container einmal kompiliert und cacht, tritt eine nicht auflösbare Abhängigkeit während des Cache-Warmups zutage und nicht zur Request-Zeit. Diese Garantie ist eine direkte Folge des Kompilierzeit-Modells und einer der Gründe, warum große Symfony-Anwendungen unter Last vorhersehbar bleiben. Für ein breiteres Set solcher Fragen gruppiert der Symfony-Technologie-Track sie nach Komponente und Schwierigkeitsgrad.
Fazit
- Den Container arbeiten lassen: Klassen unter
src/registrieren, Abhängigkeiten im Konstruktor per Type-Hint deklarieren und die manuelle YAML-Verdrahtung komplett überspringen. - Zu
#[Autowire]nur bei Nicht-Services greifen: Parameter, Umgebungsvariablen und bestimmte Service-IDs. - Interface-Mehrdeutigkeit mit
#[AsAlias]für den Standard und#[Autowire(service: ...)]oder#[Target]für explizite Zuordnungen auflösen. - Strategy- und Plugin-Muster mit
#[AutoconfigureTag], den Prioritäten von#[AsTaggedItem]und#[AutowireIterator]antreiben; auf#[AutowireLocator]wechseln, wenn lazy Nachschlagen der eifrigen Iteration vorzuziehen ist. - Teure Mitarbeiter mit
#[Autowire(lazy: true)]aufschieben, das die nativen Lazy Objects von PHP 8.4 nutzt. - Services privat und injiziert halten statt sie abzurufen, damit der Compiler den Graphen inlinen und optimieren kann.
Fang an zu üben!
Teste dein Wissen mit unseren Interview-Simulatoren und technischen Tests.
Tags
Teilen
Verwandte Artikel

Doctrine ORM: Beziehungen in Symfony meistern
Vollständiger Leitfaden zu Doctrine-ORM-Beziehungen in Symfony. OneToMany, ManyToMany, Ladestrategien und Performance-Optimierung mit praktischen Beispielen.

Symfony-Interviewfragen: Top 25 in 2026
Die 25 häufigsten Symfony-Interviewfragen. Architektur, Doctrine ORM, Services, Security, Formulare und Tests mit ausführlichen Antworten und Codebeispielen.

Symfony 7: API Platform und Best Practices
Vollstaendiger Leitfaden zu API Platform 4 mit Symfony 7. State Processors, State Providers, Serialisierungsgruppen und erweiterte Validierung fuer professionelle REST-APIs.