Wie ein Bundle Services laedt und den Container gezielt manipuliert
Die Bundle Extension ist der Mechanismus, ueber den ein Symfony Bundle seine Services im Host-Container registriert. Compiler Passes gehen einen Schritt weiter und greifen nach dem Laden aller Bundles direkt in den ContainerBuilder ein, um getaggte Services zu sammeln oder Definitionen zu veraendern. Dieser Artikel erklaert beide Mechanismen im Zusammenspiel.
Inhaltsverzeichnis
- 1. Extension und Compiler Pass im Container-Lebenszyklus
- 2. Die Extension-Klasse: load() im Detail
- 3. Service-Definitionen laden: PHP vs. YAML
- 4. Einen eigenen Compiler Pass schreiben
- 5. Compiler Pass in der Bundle-Klasse registrieren
- 6. PassConfig-Typ und Prioritaet steuern
- 7. Getaggte Services einsammeln: findTaggedServiceIds()
- 8. Typische Fallstricke bei Extension und Compiler Pass
- 9. Extension vs. Compiler Pass im Vergleich
- 10. Zusammenfassung
- 11. FAQ
1. Extension und Compiler Pass im Container-Lebenszyklus
Eine Symfony Bundle Extension und ein Compiler Pass loesen zwei unterschiedliche, aber eng verwandte Aufgaben im Lebenszyklus des Dependency-Injection-Containers. Die Extension laedt Service-Definitionen und wertet die validierte Bundle-Konfiguration aus, bevor der Container kompiliert wird. Der Compiler Pass laeuft danach, wenn bereits alle Bundles ihre Extensions ausgefuehrt haben, und kann deshalb Definitionen aus fremden Bundles lesen und veraendern, was der Extension allein nicht moeglich ist.
Diese Reihenfolge ist entscheidend: Waehrend jede Extension nur die eigenen Service-Definitionen kennt, hat ein Compiler Pass Zugriff auf den vollstaendigen ContainerBuilder mit allen zu diesem Zeitpunkt registrierten Services. Das macht Compiler Passes zum richtigen Werkzeug fuer Aufgaben wie das Einsammeln aller Services mit einem bestimmten Tag, unabhaengig davon, aus welchem Bundle sie stammen, etwa alle Event-Handler, die sich selbst als acme_audit.subscriber markiert haben.
Fuer ein Symfony Bundle mit eigener Erweiterbarkeit ist dieses Zusammenspiel zentral: Die Extension definiert die Kernfunktionalitaet, der Compiler Pass erlaubt es anderen Bundles oder dem Host-Projekt, sich per Tag in diese Funktionalitaet einzuklinken, ohne dass das Bundle die konkreten Erweiterungen zur Entwicklungszeit kennen muss.
2. Die Extension-Klasse: load() im Detail
Eine Extension-Klasse implementiert Symfony\Component\DependencyInjection\Extension\ExtensionInterface, in der Praxis meist ueber die abstrakte Basisklasse Extension, die bereits sinnvolle Standardimplementierungen fuer getAlias() mitbringt. Die zentrale Methode ist load(array $configs, ContainerBuilder $container): Sie erhaelt die noch unverarbeiteten Konfigurationsarrays aus allen Quellen und den ContainerBuilder des Host-Projekts, in den Service-Definitionen eingetragen werden.
Der erste Schritt in load() ist fast immer der Aufruf von processConfiguration(), der die rohen Konfigurationsarrays gegen den Configuration Tree Builder validiert und zu einem einzigen, typsicheren Array zusammenfuehrt. Danach laedt die Methode die eigentlichen Service-Definitionen, meist ueber einen PhpFileLoader oder YamlFileLoader, und setzt Container-Parameter aus den validierten Konfigurationswerten, damit Services diese per Konstruktor-Argument referenzieren koennen.
// src/DependencyInjection/AcmeAuditExtension.php
declare(strict_types=1);
namespace Acme\AuditBundle\DependencyInjection;
use Symfony\Component\Config\FileLocator;
use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\DependencyInjection\Extension\Extension;
use Symfony\Component\DependencyInjection\Loader\PhpFileLoader;
/**
* Loads services and processes configuration for the audit bundle.
*/
final class AcmeAuditExtension extends Extension
{
public function load(array $configs, ContainerBuilder $container): void
{
$configuration = new Configuration();
$config = $this->processConfiguration($configuration, $configs);
$container->setParameter('acme_audit.table_name', $config['table_name']);
$container->setParameter('acme_audit.retention_days', $config['retention_days']);
$loader = new PhpFileLoader($container, new FileLocator(__DIR__ . '/../../config'));
$loader->load('services.php');
}
}
3. Service-Definitionen laden: PHP vs. YAML
Fuer die eigentliche Service-Registrierung stehen einer Symfony Bundle Extension mehrere Loader-Klassen zur Verfuegung: PhpFileLoader fuer eine PHP-Konfigurationsdatei mit der modernen ContainerConfigurator-Syntax, YamlFileLoader fuer klassische YAML-Definitionen und XmlFileLoader fuer XML, was bei offiziellen Symfony-Bundles noch haeufig zu finden ist, weil XML eine stabile Schema-Validierung erlaubt. Fuer neue, interne Bundles ist die PHP-Variante meist die praktischste Wahl, weil sie IDE-Unterstuetzung, Autovervollstaendigung und Refactoring-Werkzeuge nutzt.
Innerhalb der services.php-Datei stehen dieselben Konstrukte wie in der Anwendungs-Konfiguration zur Verfuegung: services()->set() fuer einzelne Definitionen, autowire() und autoconfigure() fuer automatische Verdrahtung, allerdings beschraenkt auf den Namespace des Bundles selbst. Eine haeufige Best Practice: Nur die Services als public markieren, die tatsaechlich von aussen referenziert werden sollen, alle internen Implementierungsdetails bleiben privat und damit nicht direkt aus dem Host-Container abrufbar.
// config/services.php inside the bundle
declare(strict_types=1);
use Acme\AuditBundle\EventListener\AuditLogListener;
use Acme\AuditBundle\Service\AuditLogger;
use Symfony\Component\DependencyInjection\Loader\Configurator\ContainerConfigurator;
return static function (ContainerConfigurator $configurator): void {
$services = $configurator->services()
->defaults()
->autowire()
->autoconfigure();
// Public: consuming applications may inject this service directly
$services->set(AuditLogger::class)
->public()
->arg('$tableName', '%acme_audit.table_name%');
// Private: only used internally through event tagging
$services->set(AuditLogListener::class)
->tag('kernel.event_subscriber');
};
4. Einen eigenen Compiler Pass schreiben
Ein Compiler Pass implementiert Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface mit genau einer Methode: process(ContainerBuilder $container). Innerhalb dieser Methode besteht voller Zugriff auf alle bereits registrierten Service-Definitionen, unabhaengig davon, aus welchem Bundle sie stammen. Ein typischer Anwendungsfall in einem Symfony Bundle: Alle Services mit einem bestimmten Tag einsammeln und als Argumente in einen zentralen Sammel-Service, etwa einen Dispatcher oder eine Registry, injizieren.
Der Compiler Pass sollte defensiv programmiert sein, weil er zu einem Zeitpunkt laeuft, an dem noch nicht garantiert ist, dass alle erwarteten Services existieren. Ein has()-Check vor jedem getDefinition()-Aufruf verhindert, dass der Container-Build mit einer schwer nachvollziehbaren Exception abbricht, falls ein Host-Projekt einen erwarteten Service etwa durch eine Konfigurationsaenderung deaktiviert hat.
// src/DependencyInjection/Compiler/AuditSubscriberPass.php
declare(strict_types=1);
namespace Acme\AuditBundle\DependencyInjection\Compiler;
use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface;
use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\DependencyInjection\Reference;
/**
* Collects all services tagged as acme_audit.subscriber and injects
* them into the central audit dispatcher.
*/
final class AuditSubscriberPass implements CompilerPassInterface
{
public function process(ContainerBuilder $container): void
{
if (!$container->has('acme_audit.dispatcher')) {
return;
}
$dispatcherDefinition = $container->getDefinition('acme_audit.dispatcher');
$taggedServices = $container->findTaggedServiceIds('acme_audit.subscriber');
foreach ($taggedServices as $id => $tags) {
$dispatcherDefinition->addMethodCall('addSubscriber', [new Reference($id)]);
}
}
}
5. Compiler Pass in der Bundle-Klasse registrieren
Ein Compiler Pass wird nicht wie eine normale Service-Definition ueber die Extension geladen, sondern in der build()-Methode der Bundle-Klasse ueber $container->addCompilerPass() registriert. Diese Trennung ist beabsichtigt: build() laeuft fuer alle Bundles vor dem eigentlichen Laden der Extensions, sodass ein Compiler Pass unabhaengig davon registriert werden kann, ob und wie die Extension konfiguriert wurde.
Wichtig bei mehreren Compiler Passes im selben Bundle: Jeder Pass sollte genau eine Verantwortung haben, statt mehrere unabhaengige Aufgaben in einer einzigen process()-Methode zu buendeln. Das erleichtert sowohl das Testen einzelner Passes als auch die spaetere Erweiterung, wenn ein Bundle waechst und neue Cross-Cutting-Concerns dazukommen.
6. PassConfig-Typ und Prioritaet steuern
addCompilerPass() akzeptiert neben dem Pass-Objekt selbst zwei weitere Parameter: den PassConfig-Typ und eine Prioritaet. Der Typ bestimmt, in welcher der vordefinierten Phasen der Pass laeuft, etwa PassConfig::TYPE_BEFORE_OPTIMIZATION fuer Passes, die vor der Entfernung unbenutzter privater Services laufen sollen, oder PassConfig::TYPE_AFTER_REMOVING fuer Passes, die erst nach dieser Bereinigung greifen duerfen. Diese Phasenwahl entscheidet, ob ein Compiler Pass noch private, potenziell zu entfernende Services sehen kann oder nicht.
Die Prioritaet als Integer-Wert steuert die Reihenfolge innerhalb derselben Phase: Ein hoeherer Wert laeuft frueher. Das wird relevant, wenn mehrere Compiler Passes aus unterschiedlichen Bundles denselben Sammel-Service befuellen und eine bestimmte Reihenfolge, etwa erst Validierungs-Handler, dann Transformations-Handler, fachlich vorgeschrieben ist.
// src/AcmeAuditBundle.php — registering with an explicit pass type and priority
use Symfony\Component\DependencyInjection\Compiler\PassConfig;
public function build(ContainerBuilder $container): void
{
parent::build($container);
$container->addCompilerPass(
new AuditSubscriberPass(),
PassConfig::TYPE_BEFORE_OPTIMIZATION,
priority: 10
);
}
7. Getaggte Services einsammeln: findTaggedServiceIds()
findTaggedServiceIds() ist die zentrale Methode, mit der ein Compiler Pass alle Services findet, die sich per Tag selbst als Erweiterung registriert haben. Sie gibt ein assoziatives Array zurueck, dessen Schluessel die Service-IDs sind und dessen Werte die Attribute enthalten, die dem Tag mitgegeben wurden. Diese Attribute erlauben feingranulare Steuerung, etwa eine Prioritaet pro Subscriber, ohne dass der Compiler Pass selbst wissen muss, wie viele Subscriber existieren oder aus welchem Bundle sie stammen.
Fuer Symfony Bundles, die selbst erweiterbar sein sollen, ist diese Kombination aus Tag und Compiler Pass der etablierte Mechanismus: Das Bundle definiert einen Tag-Namen als oeffentliche Konvention, etwa acme_audit.subscriber, und jedes andere Bundle oder das Host-Projekt kann eigene Services mit diesem Tag markieren, ohne das Audit-Bundle selbst aendern zu muessen. Autoconfigure kann diesen Tag sogar automatisch anhand eines Interfaces vergeben, was manuelles Tagging in vielen Faellen ueberfluessig macht.
# Inspect which services carry a given tag after the container has compiled
bin/console debug:container --tag=acme_audit.subscriber
# Output shows every tagged service id, useful to verify a Compiler Pass
# actually picked up all expected subscribers before debugging further
8. Typische Fallstricke bei Extension und Compiler Pass
Der haeufigste Fehler: Ein Compiler Pass greift auf einen Service zu, bevor sichergestellt ist, dass dieser existiert, was zu einer ServiceNotFoundException fuehrt, die fuer Nutzer des Bundles schwer nachzuvollziehen ist. Ein zweiter Fallstrick betrifft die Phasenwahl: Wird ein Compiler Pass in der falschen PassConfig-Phase registriert, kann er versehentlich Services sehen, die eigentlich schon entfernt sein sollten, oder umgekehrt Services verpassen, die erst spaeter im Prozess entstehen.
Ein dritter Fehler ist die Vermischung von Verantwortlichkeiten: Konfigurationsverarbeitung gehoert in die Extension, container-uebergreifende Manipulation in den Compiler Pass. Wird versucht, in load() bereits Definitionen aus fremden Bundles zu lesen, schlaegt das haeufig fehl oder liefert unvollstaendige Ergebnisse, weil zu diesem Zeitpunkt noch nicht alle Extensions gelaufen sind. Diese Reihenfolge sauber einzuhalten ist der wichtigste Grundsatz beim Arbeiten mit Extension-Klasse und Compiler Pass.
9. Extension vs. Compiler Pass im Vergleich
Beide Mechanismen ergaenzen sich, haben aber klar unterschiedliche Einsatzbereiche innerhalb eines Symfony Bundles.
| Kriterium | Extension | Compiler Pass |
|---|---|---|
| Ausfuehrungszeitpunkt | Beim Laden des jeweils eigenen Bundles | Nach dem Laden aller Bundle-Extensions |
| Sichtbare Services | Nur die eigenen | Alle registrierten Services |
| Hauptaufgabe | Service-Definitionen laden, Konfiguration auswerten | Definitionen anderer Bundles lesen und veraendern |
| Typischer Anwendungsfall | Parameter aus Configuration Tree setzen | Getaggte Services einsammeln und verdrahten |
| Registrierung | Automatisch per Namenskonvention | Manuell via addCompilerPass() in build() |
Wer beide Mechanismen richtig einsetzt, baut ein Symfony Bundle, das selbst erweiterbar ist: Die Extension liefert die Kernfunktionalitaet, der Compiler Pass oeffnet Erweiterungspunkte fuer andere Bundles und das Host-Projekt, ohne dass eine harte Kopplung an konkrete Klassen entsteht.
Mironsoft
Symfony Bundle-Architektur, Extension-Klassen und Compiler Passes
Euer internes Bundle soll erweiterbar werden?
Wir entwerfen Extension-Klassen und Compiler Passes fuer eure Symfony Bundles, mit sauberen Erweiterungspunkten per Tag, korrekter PassConfig-Phase und defensiver Fehlerbehandlung im Container-Build.
Extension-Design
Saubere Trennung von Konfiguration, Parametern und Service-Definitionen
Compiler-Pass-Entwicklung
Tag-basierte Erweiterungspunkte fuer andere Bundles und das Host-Projekt
Debugging
Container-Kompilierung analysieren und Reihenfolge-Probleme aufloesen
10. Zusammenfassung
Eine Symfony Bundle Extension laedt Service-Definitionen und wertet die Bundle-Konfiguration ueber processConfiguration() aus, mit Zugriff nur auf die eigenen Definitionen. Ein Compiler Pass laeuft danach, wenn alle Extensions bereits geladen haben, und kann deshalb den vollstaendigen ContainerBuilder lesen und veraendern, etwa um mit findTaggedServiceIds() getaggte Services einzusammeln und in einen zentralen Dienst zu injizieren.
Registriert wird ein Compiler Pass ueber addCompilerPass() in der build()-Methode der Bundle-Klasse, mit optionaler Steuerung von PassConfig-Phase und Prioritaet. Defensive Pruefungen mit has() vor jedem getDefinition()-Zugriff verhindern schwer nachvollziehbare Fehler, wenn erwartete Services fehlen. Wer Extension und Compiler Pass sauber trennt, baut Symfony Bundles, die selbst erweiterbar sind, ohne harte Kopplung an konkrete Implementierungen anderer Bundles.
Symfony Bundle Extension und Compiler Passes — Das Wichtigste auf einen Blick
Extension-Klasse
load() verarbeitet Konfiguration und laedt Service-Definitionen, nur mit Zugriff auf eigene Services.
Compiler Pass
process() laeuft nach allen Extensions, mit Zugriff auf den vollstaendigen ContainerBuilder.
Registrierung
addCompilerPass() in build() der Bundle-Klasse, mit optionaler Phase und Prioritaet.
Erweiterbarkeit
Tag-Konvention plus findTaggedServiceIds() erlaubt anderen Bundles, sich einzuklinken.