SF
{ }
Symfony · Dependency Injection · Compiler Pass · Service Dekorator
Symfony DI:
Dekoratoren und Compiler Passes

Der Symfony Dependency-Injection-Container ist mehr als ein Service-Locator. Dekoratoren ermöglichen es, fremde Services zu erweitern, ohne ihren Code zu ändern. Compiler Passes manipulieren den Container zur Build-Zeit und automatisieren Konfigurationen, die sonst manuell gepflegt werden müssten.

17 Min. Lesezeit Dekoratoren · Compiler Passes · Service Tags · Container Passes Symfony 7.x · PHP 8.3+

1. DI-Container-Grundlagen: wie Symfony Services verwaltet

Der Symfony Dependency Injection Container ist eine kompilierte PHP-Klasse, die alle Service-Definitionen enthält. Beim ersten Aufruf in einer Umgebung kompiliert Symfony den Container aus allen services.yaml-Dateien, Bundle-Konfigurationen und programmatisch registrierten Services in eine fertige PHP-Klasse, die in var/cache/prod/ abgelegt wird. Diese kompilierte Klasse enthält keine Abstraktion mehr — Services werden direkt instanziiert, Abhängigkeiten sind konkrete Objekte. Das macht den Symfony-Container zur Laufzeit extrem schnell und erlaubt zur Build-Zeit komplexe Transformationen, die in anderen Frameworks zur Laufzeit stattfinden.

Der Symfony DI-Container unterscheidet zwischen Shared Services (Singletons, Standard) und nicht-shared Services. Für die meisten Anwendungsfälle — Repositories, Manager, Handler — sind Singletons das richtige Modell: Eine Instanz pro Request, die im Container gecacht wird. Der Kompilierungsprozess durchläuft mehrere Phasen, in denen Compiler Passes den Container manipulieren können. Diese Passes sind der Extension-Point für Bundle-Autoren und für Anwendungsentwickler, die Container-weite Transformationen automatisieren wollen, ohne jeden Service einzeln anzufassen.

2. Service-Dekoratoren: Services transparent erweitern

Der Service-Dekorator ist das Mittel der Wahl, wenn ein bestehender Service erweitert werden soll, ohne seinen Code zu ändern. Das klassische Szenario: Ein Third-Party-Bundle definiert einen LoggerInterface-Service, und die eigene Anwendung möchte jeden Log-Eintrag mit einem Kontext-Prefix versehen. Mit einem Dekorator wird ein eigener Logger-Service registriert, der den Original-Logger empfängt, Aufrufe delegiert und vorher oder nachher eigene Logik ausführt. Aus Sicht aller anderen Services, die den Logger injiziert bekommen, ändert sich nichts — sie bekommen weiterhin einen LoggerInterface-Service, der nur zufällig alle Aufrufe um eigene Logik anreichert.

Das Dekoratormuster in Symfony funktioniert durch Umbenennung: Der originale Service-ID Psr\Log\LoggerInterface wird intern zu Psr\Log\LoggerInterface.inner umbenannt, und der Dekorator übernimmt die ursprüngliche ID. Alle anderen Services, die den ursprünglichen Service als Abhängigkeit deklariert haben, bekommen nun automatisch den Dekorator. Das ist der entscheidende Unterschied zur manuellen Komposition: Kein bestehender Code muss geändert werden — weder der originale Service noch die Services, die ihn nutzen. Der Symfony DI-Dekorator greift transparent in die Abhängigkeitskette ein.


<?php

declare(strict_types=1);

namespace App\Service\Logger;

use Psr\Log\LoggerInterface;
use Symfony\Component\DependencyInjection\Attribute\AsDecorator;
use Symfony\Component\DependencyInjection\Attribute\AutowireDecorated;

/**
 * Decorates the main logger to prepend application context to all log messages.
 * All services injecting LoggerInterface automatically receive this decorator.
 */
#[AsDecorator(decorates: 'monolog.logger')]
final class ContextualLogger implements LoggerInterface
{
    public function __construct(
        #[AutowireDecorated]
        private readonly LoggerInterface $inner,
        private readonly string $context = 'app',
    ) {}

    /**
     * Forward log call with context prefix — inner logger handles actual output.
     */
    public function log(mixed $level, string|\Stringable $message, array $context = []): void
    {
        // Prepend application context to every log entry
        $prefixedMessage = "[{$this->context}] {$message}";
        $this->inner->log($level, $prefixedMessage, $context);
    }

    // All other LoggerInterface methods delegate to inner logger unchanged
    public function emergency(string|\Stringable $message, array $context = []): void
    {
        $this->log('emergency', $message, $context);
    }

    public function alert(string|\Stringable $message, array $context = []): void
    {
        $this->log('alert', $message, $context);
    }

    public function critical(string|\Stringable $message, array $context = []): void
    {
        $this->log('critical', $message, $context);
    }

    public function error(string|\Stringable $message, array $context = []): void
    {
        $this->log('error', $message, $context);
    }

    public function warning(string|\Stringable $message, array $context = []): void { $this->log('warning', $message, $context); }
    public function notice(string|\Stringable $message, array $context = []): void  { $this->log('notice', $message, $context); }
    public function info(string|\Stringable $message, array $context = []): void    { $this->log('info', $message, $context); }
    public function debug(string|\Stringable $message, array $context = []): void   { $this->log('debug', $message, $context); }
}

3. Der #[AsDecorator]-Attribut in Symfony 7

Seit Symfony 6.1 ist der Symfony DI-Dekorator durch das PHP-Attribut #[AsDecorator] konfigurierbar, ohne dass eine services.yaml-Konfiguration notwendig ist. Das Attribut trägt als Parameter die Service-ID des zu dekorierenden Services. Optional kann mit dem Parameter priority gesteuert werden, in welcher Reihenfolge mehrere Dekoratoren eines Services angewendet werden — der Dekorator mit der höchsten Priorität wird als äußerste Schicht verwendet. Das ist relevant, wenn mehrere Bundles oder eigene Services denselben Service dekorieren: Logging-Dekorator außen, Caching-Dekorator innen beispielsweise.

Der Companion #[AutowireDecorated] injiziert automatisch den dekorierten (inneren) Service in den Konstruktor des Dekorators, ohne dass die innere Service-ID manuell als String angegeben werden muss. Symfony verwaltet die .inner-ID intern. Der Dekorator muss dasselbe Interface implementieren wie der dekorierte Service, damit alle Abhängigkeiten auf das Interface typisiert bleiben und keine konkreten Klassen referenzieren müssen. Das ist gleichzeitig das SOLID-Prinzip der Dependency Inversion in der Praxis: Code hängt von Abstraktionen ab, nicht von Implementierungen.

4. Compiler Passes: den Container zur Build-Zeit manipulieren

Ein Compiler Pass ist eine PHP-Klasse, die während der Container-Kompilierung ausgeführt wird und die Container-Definitionen transformiert. Der Container ist zu diesem Zeitpunkt noch veränderbar — alle Service-Definitionen liegen als Definition-Objekte vor, die gelesen, modifiziert und ersetzt werden können. Compiler Passes lösen Probleme, die zur Laufzeit zu teuer oder gar nicht möglich wären: Sie finden alle Services mit einem bestimmten Tag, injizieren sie als Liste in einen anderen Service und entfernen den Tag danach — alles vor der ersten HTTP-Request.

Das bekannteste Beispiel eines Compiler Passes in Symfony selbst ist der Event-Dispatcher-Pass: Er findet alle Services, die mit kernel.event_listener getaggt sind, liest die Tag-Attribute (Event-Name, Methode, Priorität) aus und konfiguriert den Event-Dispatcher entsprechend. Das Resultat ist eine vollständig konfigurierte Listener-Kette ohne Laufzeit-Reflection und ohne manuelle Registrierung durch den Entwickler. Derselbe Mechanismus steht eigenen Bundles und Anwendungen zur Verfügung — eigene Passes können mit demselben Grad an Automatisierung arbeiten.


<?php

declare(strict_types=1);

namespace App\DependencyInjection\Compiler;

use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface;
use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\DependencyInjection\Reference;

/**
 * Compiler Pass: collect all services tagged with app.payment_gateway
 * and inject them as a prioritized list into PaymentGatewayRegistry.
 */
final class PaymentGatewayPass implements CompilerPassInterface
{
    /**
     * Find all tagged payment gateways and wire them into the registry service.
     */
    public function process(ContainerBuilder $container): void
    {
        // Skip if registry service was removed or not defined
        if (!$container->has('App\Payment\PaymentGatewayRegistry')) {
            return;
        }

        $registryDefinition = $container->findDefinition('App\Payment\PaymentGatewayRegistry');

        // Find all services tagged with app.payment_gateway, sorted by priority attribute
        $taggedServices = $container->findTaggedServiceIds('app.payment_gateway', true);

        $gateways = [];
        foreach ($taggedServices as $serviceId => $tags) {
            foreach ($tags as $tag) {
                $priority = $tag['priority'] ?? 0;
                $gateways[$priority][] = new Reference($serviceId);
            }
        }

        // Sort by priority descending — higher priority gateways checked first
        krsort($gateways);
        $sortedGateways = array_merge(...array_values($gateways));

        // Inject the sorted gateway list into the registry constructor argument
        $registryDefinition->setArgument('$gateways', $sortedGateways);
    }
}

5. Service Tags: Gruppen von Services automatisch verarbeiten

Service Tags sind der Mechanismus, durch den Symfony DI Services in Gruppen kategorisiert. Ein Tag ist ein Name mit optionalen Attributen, der an eine Service-Definition gehängt wird. Der kernel.event_listener-Tag ist der bekannteste, aber das Muster ist universell anwendbar: app.payment_gateway, app.import_handler, app.validator — jeder Name, der in der eigenen Anwendung sinnvoll ist. Ein Compiler Pass findet dann alle Services mit diesem Tag und verarbeitet sie automatisch.

In Symfony 7 können eigene Tags über PHP-Attribute deklariert werden. Mit #[AutoconfigureTag('app.payment_gateway', ['priority' => 10])] auf der Klasse wird der Service automatisch mit dem Tag und der Priorität registriert, wenn Autoconfiguration aktiv ist. Das vermeidet manuelle services.yaml-Einträge für jeden neuen Gateway und reduziert Konfigurationsaufwand auf Null: Ein neuer Gateway implementiert das Interface, trägt das Attribut und ist automatisch Teil des Systems — ohne manuelle Registrierung und ohne Änderungen an der bestehenden Konfiguration. Der Compiler Pass findet ihn beim nächsten Cache-Clearing automatisch.

6. Eigene Compiler Passes schreiben und registrieren

Ein eigener Compiler Pass wird in der Kernel-Klasse oder in der Bundle-Klasse registriert. In der Anwendung ohne eigenes Bundle ist die Kernel.php der richtige Ort: Die Methode build(ContainerBuilder $container) wird während der Container-Kompilierung aufgerufen und ist der Extension-Point für eigene Passes. Mit $container->addCompilerPass(new PaymentGatewayPass()) wird der Pass registriert. Die Reihenfolge mehrerer Passes wird durch den zweiten Parameter gesteuert: PassConfig::TYPE_BEFORE_OPTIMIZATION, TYPE_OPTIMIZE, TYPE_BEFORE_REMOVING, TYPE_REMOVE und TYPE_AFTER_REMOVING sind die verfügbaren Phasen.

Die Phase bestimmt, welche anderen Passes bereits ausgeführt wurden und was im Container noch verfügbar ist. Ein Pass in der BEFORE_OPTIMIZATION-Phase sieht alle Services, auch solche, die später noch entfernt werden. Ein Pass in der AFTER_REMOVING-Phase sieht nur noch Services, die tatsächlich im finalen Container landen. Für die meisten eigenen Passes ist die Standard-Phase TYPE_BEFORE_OPTIMIZATION korrekt. Symfony's interne Passes laufen zu definierten Zeiten — eigene Passes können mit dem dritten Parameter priority feingradig innerhalb einer Phase positioniert werden, wenn die Reihenfolge relativ zu anderen Passes relevant ist.


<?php

declare(strict_types=1);

// src/Kernel.php — Register custom compiler pass in application kernel
namespace App;

use App\DependencyInjection\Compiler\PaymentGatewayPass;
use App\DependencyInjection\Compiler\ImportHandlerChainPass;
use Symfony\Bundle\FrameworkBundle\Kernel\MicroKernelTrait;
use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\DependencyInjection\Compiler\PassConfig;
use Symfony\Component\HttpKernel\Kernel as BaseKernel;

class Kernel extends BaseKernel
{
    use MicroKernelTrait;

    /**
     * Register custom compiler passes — called during container compilation.
     */
    protected function build(ContainerBuilder $container): void
    {
        // Default phase: TYPE_BEFORE_OPTIMIZATION (most common use case)
        $container->addCompilerPass(new PaymentGatewayPass());

        // Explicit phase: runs after Symfony's optimization passes
        $container->addCompilerPass(
            new ImportHandlerChainPass(),
            PassConfig::TYPE_BEFORE_REMOVING,
            priority: 10, // Higher priority = runs first within this phase
        );
    }
}

// src/Payment/PaymentGatewayRegistry.php — Receives the injected gateway list
final class PaymentGatewayRegistry
{
    /** @param iterable<PaymentGatewayInterface> $gateways */
    public function __construct(
        private readonly iterable $gateways,
    ) {}

    public function findGateway(string $method): ?PaymentGatewayInterface
    {
        foreach ($this->gateways as $gateway) {
            if ($gateway->supports($method)) {
                return $gateway;
            }
        }
        return null;
    }
}

7. Container debuggen und Pass-Reihenfolge verstehen

Der Symfony DI-Container lässt sich mit dem Konsolenbefehlen bin/console debug:container und bin/console debug:autowiring inspizieren. debug:container --tag=app.payment_gateway listet alle Services, die mit einem bestimmten Tag versehen sind — ein schneller Check, ob ein neuer Service korrekt getaggt wurde. debug:container PaymentGatewayRegistry zeigt die vollständige Definition eines Services inklusive aller injizierten Argumente nach der Compiler-Pass-Verarbeitung. Das ist der erste Schritt beim Debuggen von Compiler-Pass-Problemen: Was ist im finalen Container-Build der Wert des Arguments, das der Pass gesetzt hat?

Um zu verstehen, welche Compiler Passes in welcher Reihenfolge ausgeführt werden, gibt bin/console debug:container --show-private tiefe Einblicke in die Container-Konfiguration. Für Compiler-Pass-Debugging empfiehlt sich der Symfony Profiler in der dev-Umgebung: Der Container-Tab zeigt alle Service-Definitionen mit ihren Argumenten nach der Pass-Verarbeitung. Wer einen Pass debuggen will, kann temporär einen dump($container->getDefinition('service.id')->getArguments()) in den Pass einfügen — das gibt die aktuellen Argumente der Definition vor und nach dem Pass-Eingriff aus.

8. Typische Muster: Chain of Responsibility mit Compiler Pass

Das Chain-of-Responsibility-Muster ist eines der häufigsten Anwendungsfälle für Symfony DI Compiler Passes. Das Muster: Mehrere Handler verarbeiten eine Anfrage nacheinander, jeder Handler entscheidet, ob er die Anfrage behandelt oder an den nächsten weiterleitet. In Symfony wird dieses Muster durch eine Kette von Services implementiert, die alle dasselbe Interface implementieren und automatisch durch einen Compiler Pass in der richtigen Reihenfolge verknüpft werden. Das Ergebnis ist ein vollständig automatisiertes System: Neuer Handler implementiert Interface, trägt Tag, ist automatisch Teil der Kette.

Wichtig bei diesem Muster: Die Priorität der Tags bestimmt die Reihenfolge in der Kette. Höhere Priorität bedeutet, der Handler wird früher aufgerufen. Das ist in der services.yaml oder im #[AutoconfigureTag]-Attribut konfigurierbar. Der Compiler Pass liest die Prioritäten aus den Tag-Attributen, sortiert die Handler und injiziert sie in dieser Reihenfolge. Zur Laufzeit gibt es keinen Overhead für das Sortieren — das ist bereits zur Build-Zeit erledigt und liegt als fertige Array-Konfiguration im kompilierten Container.

9. Dekorator vs. Preference vs. Event: Vergleich der Ansätze

In Symfony gibt es mehrere Wege, Services zu erweitern oder zu verändern. Die Wahl des richtigen Ansatzes hängt davon ab, was genau erweitert werden soll und wie stark der Eingriff ist. Symfony DI-Dekoratoren sind der sauberste Ansatz für transparente Erweiterungen eines Services ohne Code-Änderung am Original. Preferences (Service-Aliase) ersetzen eine Klasse vollständig durch eine andere ohne Delegation — keine Möglichkeit, die Original-Logik zu rufen. Events sind für lose Kopplung gedacht, wenn der erweiternde Code nicht zwingend auf das Ergebnis des originalen Codes zugreifen muss.

Ansatz Zugriff auf Original Transparenz Bester Einsatz
DI-Dekorator Ja — delegiert an Inner Vollständig transparent Logging, Caching, Tracing
Service-Alias (Preference) Nein — ersetzt vollständig Vollständige Ersetzung Alternative Implementierungen
Event/Hook Nur über Event-Daten Lose Kopplung Benachrichtigungen, Side-Effects
Compiler Pass Container-weite Sicht Build-Zeit, kein Laufzeit-Overhead Tag-basierte Automatisierung
Middleware / Stack Ja — Pipeline-Delegation Explizit konfiguriert HTTP-Request-Verarbeitung

Die Kombination von Compiler Passes und Dekoratoren ist besonders mächtig: Ein Compiler Pass findet alle Services mit einem Tag, ein Dekorator wrapping den gesamten Service-Stack. Das ist das Muster, das Symfony selbst für den Event-Dispatcher, den Messenger-Bus und die Security-Voter-Kette verwendet. Wer dieses Muster versteht, versteht, wie Symfony die gesamte eigene Infrastruktur aufbaut — und kann mit denselben Mitteln eigene, gleichwertig flexible Infrastruktur aufbauen.

Mironsoft

Symfony-Architektur, DI-Container-Design und Bundle-Entwicklung

Symfony DI-Architektur professionell aufbauen?

Wir entwerfen skalierbare Symfony-DI-Konfigurationen mit Dekoratoren, Compiler Passes und Tag-basierten Automatisierungen — von der ersten Service-Definition bis zum produktionsreifen Bundle.

DI-Audit

Bestehende Service-Definitionen analysieren und Dekorator- sowie Pass-Potenziale identifizieren

Bundle-Entwicklung

Wiederverwendbare Symfony-Bundles mit eigenem Extension-Point und Compiler Passes bauen

Architekturberatung

DI-Muster-Auswahl, Dekorator- vs. Event-Entscheidungen und Container-Optimierung

10. Zusammenfassung

Symfony DI-Dekoratoren und Compiler Passes sind die zwei mächtigsten Erweiterungs-Mechanismen des Symfony-Containers. Dekoratoren erlauben es, Services transparent zu erweitern, ohne deren Code zu ändern und ohne dass andere Services wissen, dass sie einen Dekorator statt des Originals erhalten. Das #[AsDecorator]-Attribut macht die Konfiguration idiomatisch und typsicher. Compiler Passes manipulieren den Container zur Build-Zeit und automatisieren Tag-basierte Service-Verknüpfungen, die ohne Passes manuell gepflegt werden müssten.

Die Kombination beider Mechanismen ist das Fundament der Symfony-eigenen Infrastruktur: Event-Dispatcher, Messenger-Bus, Security-Voter und HTTP-Middleware-Stack werden alle mit Compiler Passes und Dekoratoren aufgebaut. Das gleiche Werkzeug steht Anwendungsentwicklern und Bundle-Autoren zur Verfügung. Wer Compiler Passes und Dekoratoren beherrscht, kann eigene Frameworks und Bundles bauen, die dieselbe Flexibilität und Erweiterbarkeit haben wie Symfony selbst — mit null Laufzeit-Overhead, weil die gesamte Konfiguration zur Build-Zeit abgeschlossen ist.

Symfony DI Dekoratoren und Compiler Passes — Das Wichtigste auf einen Blick

Service-Dekorator

#[AsDecorator] + #[AutowireDecorated] erweitern Services transparent. Alle Abhängigkeiten erhalten automatisch den Dekorator statt des Originals.

Compiler Pass

Implementiert CompilerPassInterface, findet getaggte Services mit findTaggedServiceIds() und injiziert sie als Liste — null Laufzeit-Overhead.

Service Tags

#[AutoconfigureTag] auf der Klasse — neuer Service mit Interface und Tag ist automatisch Teil des Systems, ohne manuelle Konfiguration.

Debugging

bin/console debug:container --tag=app.my_tag und debug:container ServiceClass zeigen den finalen Container-Zustand nach allen Passes.

11. FAQ: Symfony DI Dekoratoren und Compiler Passes

1Was ist ein Service-Dekorator?
Erweitert einen Service transparent, ohne seinen Code zu ändern. Implementiert dasselbe Interface, delegiert an den originalen Service und fügt eigene Logik hinzu.
2Was ist ein Compiler Pass?
PHP-Klasse, die während der Container-Kompilierung Service-Definitionen manipuliert. Ergebnis landet im kompilierten Container — kein Laufzeit-Overhead.
3#[AsDecorator] vs. Service-Aliasing?
AsDecorator injiziert das Original als inner — der Dekorator kann es aufrufen. Aliasing ersetzt vollständig ohne Zugriff auf das Original.
4Compiler Pass registrieren?
In Kernel.php: build(ContainerBuilder $container) überschreiben und $container->addCompilerPass(new MyPass()) aufrufen. Läuft bei jedem Cache-Clearing.
5Was sind Service Tags?
Kategorisieren Services in Gruppen. Compiler Pass findet sie per findTaggedServiceIds(). #[AutoconfigureTag] setzt Tags automatisch per PHP-Attribut.
6Reihenfolge der Passes?
Fünf Phasen: BEFORE_OPTIMIZATION, OPTIMIZE, BEFORE_REMOVING, REMOVE, AFTER_REMOVING. Innerhalb einer Phase: priority-Parameter. Standard: BEFORE_OPTIMIZATION mit Priorität 0.
7Mehrere Dekoratoren mit Priorität?
priority-Parameter in #[AsDecorator]. Höhere Priorität = äußerste Schicht. Zwiebelschicht: äußerer ruft inneren, der nächsten, bis Original erreicht ist.
8Compiler Pass debuggen?
debug:container ServiceClass zeigt finalen Stand. --tag listet getaggte Services. Temporär dump() im Pass einfügen für Zwischenstände.
9Compiler Pass vs. Event-Listener?
Compiler Pass: statische Konfiguration zur Build-Zeit, kein Laufzeit-Overhead. Event-Listener: Reaktion auf Laufzeit-Ereignisse mit Zugriff auf Objekte und Zustände.
10Laufzeit-Overhead des Containers?
Keiner. In Production ist der Container eine kompilierte PHP-Klasse. Services direkt instanziiert, Passes hart verdrahtet — kein Mehraufwand gegenüber manuell geschriebenem Code.