Event Listener ohne YAML-Konfiguration
Wer in Symfony Event Listener bisher über services.yaml mit Tags und Methodennamen registriert hat, kennt die Pflege mehrerer Dateien für eine einzige Klasse. Das PHP-Attribut #[AsEventListener] eliminiert diese YAML-Konfiguration vollständig — der Listener ist direkt an der Klasse oder Methode deklariert, typsicher und sofort lesbar.
Inhaltsverzeichnis
- 1. Das Problem mit YAML-Konfiguration für Event Listener
- 2. Das Attribut #[AsEventListener] im Detail
- 3. Kernel-Events mit #[AsEventListener] behandeln
- 4. Mehrere Events in einer Listener-Klasse
- 5. Prioritäten und Ausführungsreihenfolge steuern
- 6. Eigene Events definieren und abonnieren
- 7. Event-Propagation stoppen
- 8. EventSubscriber vs. AsEventListener: Wann was?
- 9. Vergleich: YAML-Konfiguration vs. PHP-Attribut
- 10. Zusammenfassung
- 11. FAQ
1. Das Problem mit YAML-Konfiguration für Event Listener
In Symfony wurden Event Listener traditionell über services.yaml registriert: ein Tag-Eintrag mit kernel.event_listener, dem Event-Namen, der Methode und optional einer Priorität. Das klingt überschaubar, solange man wenige Listener hat. In realen Projekten mit Dutzenden von Listenern entstehen aber Konfigurationsdateien, die kaum noch navigierbar sind — und bei jeder Umbenennung einer Methode muss man sowohl die PHP-Klasse als auch die YAML-Datei anpassen. Vergisst man die YAML-Datei, ist der Listener still deaktiviert, ohne Fehlermeldung.
Das eigentliche Grundproblem ist die Entkopplung von Implementierung und Konfiguration: Die PHP-Klasse enthält die Logik, die YAML-Datei enthält die Metadaten, und beide müssen manuell synchron gehalten werden. Mit PHP 8.0 und der Einführung nativer Attribute hat sich dieses Bild verändert. Symfony nutzt Attribute seit Version 6.0 konsequent, um Konfiguration direkt in den Code zu ziehen — #[AsEventListener] ist eines der deutlichsten Beispiele dafür. Das Attribut macht YAML für Event Listener vollständig überflüssig und hält Deklaration und Logik an derselben Stelle.
Ein zweiter Aspekt ist die Lesbarkeit: Wer eine Klasse öffnet und das Attribut #[AsEventListener] sieht, weiß sofort, dass es sich um einen Listener handelt, welches Event er abhört und mit welcher Priorität er aufgerufen wird — ohne in services.yaml zu suchen. Das macht Code-Reviews schneller und Onboarding neuer Entwickler einfacher. Symfony's Dependency Injection-Container verarbeitet das Attribut beim Kompilieren und generiert dieselbe interne Tag-Konfiguration, die bisher manuell in YAML stand — der Entwickler sieht sie nur nicht mehr.
2. Das Attribut #[AsEventListener] im Detail
Das Attribut #[AsEventListener] aus dem Namespace Symfony\Component\EventDispatcher\Attribute kann sowohl auf Klassen als auch auf einzelne Methoden angewendet werden. In der einfachsten Form — das Attribut direkt auf der Klasse ohne Parameter — erwartet Symfony eine __invoke-Methode, und der Event-Name wird aus dem Typ-Hint des Parameters dieser Methode abgeleitet. Das ist die kompakteste Variante: Eine Klasse, ein Event, keine zusätzliche Konfiguration außer dem Attribut.
Auf Methodenebene angewendet ist #[AsEventListener] flexibler. Der Parameter event gibt den Event-Namen explizit an, method gibt die aufzurufende Methode an (standardmäßig der Methodenname selbst), priority steuert die Ausführungsreihenfolge und dispatcher erlaubt die Registrierung an einem anderen als dem Standard-Event-Dispatcher. Alle Parameter sind optional — man gibt nur an, was von der Standardableitung abweicht. Das Attribut ist wiederholbar: Mehrere #[AsEventListener]-Attribute auf derselben Methode oder Klasse sind möglich und registrieren den Listener für mehrere Events gleichzeitig.
Damit Symfony das Attribut erkennt, muss autoconfigure: true im Container aktiv sein — was in jedem Symfony-Projekt seit Version 4.4 der Standard ist. Wer autoconfigure: false gesetzt hat, muss das Tag manuell hinzufügen, erhält aber von bin/console debug:event-dispatcher eine klare Übersicht über alle registrierten Listener. Für Projekte mit vielen Listenern ist dieser Befehl das wichtigste Debugging-Werkzeug: Er zeigt Event-Name, Klasse, Methode und Priorität in übersichtlicher Form.
<?php
declare(strict_types=1);
namespace App\EventListener;
use Symfony\Component\EventDispatcher\Attribute\AsEventListener;
use Symfony\Component\HttpKernel\Event\RequestEvent;
use Symfony\Component\HttpKernel\KernelEvents;
/**
* Simplest form: attribute on class, __invoke method, event derived from type-hint.
* No YAML configuration needed — autoconfigure: true handles registration.
*/
#[AsEventListener]
final class MaintenanceModeListener
{
public function __construct(
private readonly bool $maintenanceMode,
) {}
/**
* Check maintenance mode on every incoming request.
* Event type is derived automatically from the parameter type-hint.
*/
public function __invoke(RequestEvent $event): void
{
if (!$this->maintenanceMode) {
return;
}
// Only affect master requests, not sub-requests
if (!$event->isMainRequest()) {
return;
}
// Redirect to maintenance page or return 503
// $event->setResponse(new Response('Maintenance', 503));
}
}
// Equivalent YAML that this attribute REPLACES — never write this again:
// services:
// App\EventListener\MaintenanceModeListener:
// tags:
// - { name: kernel.event_listener, event: kernel.request, method: __invoke }
3. Kernel-Events mit #[AsEventListener] behandeln
Symfony's HTTP-Kernel löst eine definierte Reihe von Events aus, die den gesamten Request-Response-Zyklus abdecken: kernel.request, kernel.controller, kernel.controller_arguments, kernel.view, kernel.response, kernel.finish_request, kernel.exception und kernel.terminate. Alle diese Events sind in der Klasse KernelEvents als Konstanten definiert. Mit #[AsEventListener] und dem Parameter event: KernelEvents::REQUEST ist die Bindung typsicher und refactoring-freundlich — statt des Magic-Strings 'kernel.request' verwendet man die Konstante.
Besonders häufig genutzt ist kernel.exception für die zentrale Fehlerbehandlung. Ein #[AsEventListener]-Listener auf diesem Event empfängt ein ExceptionEvent-Objekt, das die Ausnahme und den aktuellen Request enthält. Der Listener kann eine Response setzen, die Symfony als finale Antwort verwendet — ohne dass die Exception den normalen Fehler-Handler erreicht. Das ermöglicht API-spezifische JSON-Fehlerantworten neben HTML-Fehlerseiten für Browser-Requests, gesteuert durch den Accept-Header.
Für kernel.response sind Sicherheits-Header ein typischer Anwendungsfall: Content-Security-Policy, Strict-Transport-Security und X-Frame-Options werden zentral im Listener gesetzt, statt in jedem Controller wiederholt zu werden. Der #[AsEventListener]-Decorator auf der Methode onKernelResponse mit event: KernelEvents::RESPONSE macht die Zuständigkeit dieser Klasse sofort klar — kein Blick in YAML nötig, keine Überraschungen bei der Ausführungsreihenfolge.
<?php
declare(strict_types=1);
namespace App\EventListener;
use Symfony\Component\EventDispatcher\Attribute\AsEventListener;
use Symfony\Component\HttpFoundation\JsonResponse;
use Symfony\Component\HttpKernel\Event\ExceptionEvent;
use Symfony\Component\HttpKernel\Event\ResponseEvent;
use Symfony\Component\HttpKernel\KernelEvents;
/**
* Handles kernel events: API error responses and security headers.
* Multiple #[AsEventListener] attributes on one class — no YAML needed.
*/
final class HttpKernelListener
{
// priority: 20 ensures this runs before Symfony's default exception handler
#[AsEventListener(event: KernelEvents::EXCEPTION, priority: 20)]
public function onException(ExceptionEvent $event): void
{
$request = $event->getRequest();
// Only handle API requests (Accept: application/json)
if (!str_contains($request->headers->get('Accept', ''), 'application/json')) {
return;
}
$exception = $event->getThrowable();
$event->setResponse(new JsonResponse([
'error' => $exception->getMessage(),
'code' => $exception->getCode(),
'type' => (new \ReflectionClass($exception))->getShortName(),
], 500));
}
#[AsEventListener(event: KernelEvents::RESPONSE)]
public function onResponse(ResponseEvent $event): void
{
if (!$event->isMainRequest()) {
return;
}
// Add security headers to every response centrally
$response = $event->getResponse();
$response->headers->set('X-Frame-Options', 'DENY');
$response->headers->set('X-Content-Type-Options', 'nosniff');
$response->headers->set('Referrer-Policy', 'strict-origin-when-cross-origin');
}
}
4. Mehrere Events in einer Listener-Klasse
Mit #[AsEventListener] auf Methodenebene kann eine einzige Klasse mehrere Events behandeln, ohne EventSubscriber sein zu müssen. Das ist besonders nützlich, wenn mehrere Events thematisch zusammengehören und dieselben Abhängigkeiten nutzen. Ein Authentifizierungs-Listener könnte sowohl security.interactive_login als auch security.logout abonnieren — beide Methoden brauchen denselben Service für Audit-Logging, und es ergibt sich keine Duplizierung von Konstruktor-Abhängigkeiten.
Das Attribut ist wiederholbar: Man schreibt es mehrfach über dieselbe Methode oder über verschiedene Methoden derselben Klasse. Symfony erzeugt für jede Attribut-Instanz einen separaten Listener-Eintrag im Container. Die Klasse selbst wird dabei nur einmal instanziiert — der Dependency Injection-Container gibt dieselbe Instanz für alle Events, sofern die Klasse nicht explizit als nicht-shared definiert ist. Das spart Speicher und vermeidet redundante Konstruktoraufrufe bei jedem Request.
Es gibt eine wichtige Designfrage: Wann ist eine Klasse mit mehreren Event-Methoden noch kohärent, und wann sollte man sie aufteilen? Die Faustregel lautet: Wenn alle Events denselben fachlichen Kontext haben (z.B. alle Benutzer-bezogenen Events), ist eine Klasse sinnvoll. Wenn Events aus verschiedenen Fachbereichen stammen, gehören sie in separate Listener-Klassen. #[AsEventListener] macht beides gleich einfach — die Entscheidung liegt rein bei der fachlichen Kohäsion, nicht bei technischen Einschränkungen der Konfiguration.
5. Prioritäten und Ausführungsreihenfolge steuern
In Symfony bestimmt die Priorität eines #[AsEventListener], in welcher Reihenfolge Listener für dasselbe Event aufgerufen werden. Höhere Zahlen bedeuten frühere Ausführung — ein Listener mit Priorität 100 wird vor einem mit Priorität 0 aufgerufen, der wiederum vor einem mit Priorität -100 läuft. Standardmäßig hat jeder Listener die Priorität 0. Der kernel.request-Router-Listener von Symfony selbst hat Priorität 32, der Security-Listener hat 8. Wer vor dem Router reagieren will, braucht eine Priorität größer als 32.
Das Prioritätssystem ist besonders wichtig bei Listenern, die voneinander abhängen. Ein Listener, der Authentifizierungsdaten aus dem Request extrahiert und am Request-Attribut anhängt, muss vor dem Listener ausgeführt werden, der diese Daten liest. Mit #[AsEventListener(event: KernelEvents::REQUEST, priority: 50)] für den ersten und #[AsEventListener(event: KernelEvents::REQUEST, priority: 10)] für den zweiten ist die Ausführungsreihenfolge dokumentiert und erzwungen — ohne externe Abhängigkeitsdokumentation.
bin/console debug:event-dispatcher kernel.request listet alle für dieses Event registrierten Listener in ihrer tatsächlichen Ausführungsreihenfolge auf — sortiert nach Priorität. Das ist das wichtigste Tool zur Diagnose von Listener-Konflikten: Wenn ein Listener unerwartet nicht ausgeführt wird, weil ein anderer die Propagation stoppt, oder wenn die Reihenfolge falsch ist, zeigt dieser Befehl die gesamte Kette auf einen Blick.
6. Eigene Events definieren und abonnieren
Nicht alle Events in Symfony stammen aus dem Kernel. Eigene Domänen-Events — etwa OrderPlacedEvent, UserRegisteredEvent oder ProductPublishedEvent — folgen denselben Mustern und profitieren genauso von #[AsEventListener]. Eine eigene Event-Klasse erbt typischerweise von Symfony\Contracts\EventDispatcher\Event und trägt die relevanten Domänendaten als readonly Properties. Der EventDispatcher wird im Service per DI injiziert und dispatch($event) aufgerufen — fertig.
Der Listener für das eigene Event trägt das Attribut #[AsEventListener(event: OrderPlacedEvent::class)] — der Event-Name ist die FQCN der Event-Klasse selbst. Symfony löst das beim Kompilieren auf und registriert den Listener korrekt. Das hat einen entscheidenden Vorteil gegenüber Magic-Strings: Wenn die Event-Klasse umbenannt oder verschoben wird, bricht der Code sofort mit einem Compile-Fehler — statt still keinen Listener mehr aufzurufen. Die Kombination aus typisiertem Event und #[AsEventListener] macht das gesamte Event-System refactoring-sicher.
Für größere Projekte empfiehlt sich das Einführen einer zentralen Event-Klasse pro Fachbereich, die alle Event-Konstanten des Bereichs enthält. So kann man Events auch als Strings benennen — UserEvents::REGISTERED statt der FQCN — und behält volle Kontrolle über den Event-Namen ohne Abhängigkeit von der Klassen-Struktur. Beide Ansätze funktionieren gleich gut mit #[AsEventListener]; die Wahl ist eine Team-Entscheidung über Namenskonventionen.
<?php
declare(strict_types=1);
namespace App\Event;
use Symfony\Contracts\EventDispatcher\Event;
/**
* Dispatched when a new order is successfully placed.
* Immutable: all data set at construction, no setters.
*/
final class OrderPlacedEvent extends Event
{
public function __construct(
public readonly int $orderId,
public readonly string $customerEmail,
public readonly float $totalAmount,
public readonly \DateTimeImmutable $placedAt = new \DateTimeImmutable(),
) {}
}
// --- Listener subscribing to this custom event ---
namespace App\EventListener;
use App\Event\OrderPlacedEvent;
use App\Service\MailerService;
use Symfony\Component\EventDispatcher\Attribute\AsEventListener;
/**
* Sends order confirmation email when an order is placed.
* Event name is derived from the FQCN — refactoring-safe.
*/
#[AsEventListener(event: OrderPlacedEvent::class)]
final class OrderConfirmationListener
{
public function __construct(
private readonly MailerService $mailer,
) {}
/**
* Handle the order placed event and dispatch confirmation email.
*/
public function __invoke(OrderPlacedEvent $event): void
{
$this->mailer->sendOrderConfirmation(
email: $event->customerEmail,
orderId: $event->orderId,
total: $event->totalAmount,
);
}
}
7. Event-Propagation stoppen
Wenn ein Listener die Verarbeitung eines Events abschließt und verhindern möchte, dass weitere Listener aufgerufen werden, ruft er $event->stopPropagation() auf. Das ist ein Standard-Feature des Symfony EventDispatchers und funktioniert unabhängig davon, ob der Listener per YAML oder per #[AsEventListener] registriert wurde. Der typische Anwendungsfall: Ein hochpriorisierter Listener prüft, ob eine spezielle Bedingung erfüllt ist (z.B. Maintenance-Modus), setzt eine Response und stoppt die Propagation — alle weiteren Listener für dieses Event werden übersprungen.
Das Stoppen der Propagation ist mit Vorsicht einzusetzen, weil es implizite Abhängigkeiten zwischen Listenern schafft: Ein Listener verlässt sich darauf, aufgerufen zu werden — aber ein anderer mit höherer Priorität kann das verhindern. bin/console debug:event-dispatcher zeigt nicht, ob ein Listener die Propagation stoppen kann; das muss man aus dem Code lesen. Eine klare Dokumentation im Listener selbst — z.B. im PHPDoc-Kommentar — ist hier besonders wichtig. Mit #[AsEventListener] ist zumindest die Priorität direkt am Listener sichtbar, was die Analyse erleichtert.
8. EventSubscriber vs. AsEventListener: Wann was?
Vor #[AsEventListener] war der EventSubscriberInterface die empfohlene Methode, um mehrere Events in einer Klasse zu behandeln: Die statische Methode getSubscribedEvents() gibt ein Array mit Event-Namen, Methoden und Prioritäten zurück. Das funktioniert weiterhin in Symfony 6 und 7, aber #[AsEventListener] ist in vielen Fällen die klarere Alternative. Der wesentliche Unterschied: EventSubscriber-Klassen sind schwerer zu testen, weil getSubscribedEvents() eine statische Methode ist, die das Event-Dispatching implizit konfiguriert. Mit #[AsEventListener]-Methoden kann man dieselbe Methode im Test direkt aufrufen, ohne den Dispatcher einzubeziehen.
EventSubscriber bleibt sinnvoll, wenn die Event-Konfiguration dynamisch ist oder zur Laufzeit variieren kann — das ist mit PHP-Attributen nicht möglich, weil Attribute zur Compile-Zeit ausgewertet werden. Für statische, testbare Event-Behandlung mit klarer Zuständigkeit ist #[AsEventListener] die modernere Wahl. In neuen Symfony-Projekten ist das Attribut der bevorzugte Ansatz für neue Listener, während bestehende EventSubscriber nicht zwingend migriert werden müssen — beide Ansätze koexistieren problemlos im selben Projekt.
| Merkmal | YAML-Konfiguration | #[AsEventListener] | EventSubscriber |
|---|---|---|---|
| Konfigurationsort | services.yaml — separate Datei | Direkt am PHP-Code | getSubscribedEvents() statisch |
| Typsicherheit | Magic-Strings, keine IDE-Hilfe | FQCN oder KernelEvents-Konstante | Magic-Strings möglich |
| Testbarkeit | Methode direkt testbar | Methode direkt testbar | Statische Methode erschwert Isolation |
| Mehrere Events | Mehrere Tag-Einträge | Wiederholbares Attribut | getSubscribedEvents-Array |
| Dynamische Konfig | Möglich über Factory | Nicht möglich (Compile-Zeit) | Möglich (PHP-Methode) |
9. Vergleich: YAML-Konfiguration vs. PHP-Attribut
Der praktische Unterschied zeigt sich deutlich bei der Migration eines bestehenden Listeners von YAML zu #[AsEventListener]: Man entfernt den Tag-Eintrag aus services.yaml, fügt das Attribut zur PHP-Klasse hinzu und räumt damit gleichzeitig eine Konfigurationszeile auf. Das Cache leeren reicht, um den Listener neu zu registrieren. Der Befehl bin/console debug:event-dispatcher zeigt danach dieselbe Ausgabe wie zuvor — derselbe Event-Name, dieselbe Methode, dieselbe Priorität. Für den Container-Kompiler ist es transparent, ob die Registrierung aus YAML oder aus einem Attribut stammt.
Die Migration von YAML zu #[AsEventListener] lohnt sich nicht nur aus Lesbarkeit-Gründen, sondern auch aus Sicherheitsgründen: Ein Listener, der in YAML auf einen Methodennamen referenziert, der in PHP umbenannt wurde, bleibt im Container registriert und ruft zur Laufzeit eine nicht existente Methode auf — mit einem Fehler, der erst zur Request-Zeit sichtbar wird. Mit #[AsEventListener] direkt auf der Methode ist die Verknüpfung untrennbar: Die Methode existiert genau dann, wenn das Attribut aktiv ist.
Mironsoft
Symfony-Entwicklung, Event-Architektur und Backend-Optimierung
Symfony Event-Architektur für euer Projekt modernisieren?
Wir migrieren bestehende YAML-Konfigurationen zu PHP-Attributen, entwerfen saubere Event-Hierarchien und integrieren Custom Events in euren Symfony-Stack — mit vollständiger Testabdeckung und Prioritäts-Dokumentation.
Event-Audit
Analyse aller registrierten Listener, Prioritätskonflikte und verwaiste YAML-Einträge im bestehenden Projekt
Migration
YAML-Tags durch #[AsEventListener]-Attribute ersetzen — mit Verifikation über debug:event-dispatcher
Custom Events
Domänen-Events entwerfen und implementieren — typsicher, immutable und refactoring-freundlich
10. Zusammenfassung
Das Attribut #[AsEventListener] in Symfony macht YAML-Konfiguration für Event Listener vollständig überflüssig. Die Deklaration von Event-Name, Methode und Priorität erfolgt direkt am PHP-Code — typsicher, refactoring-freundlich und für jeden Entwickler sofort lesbar ohne Blick in Konfigurationsdateien. Symfony verarbeitet das Attribut beim Container-Kompilieren und generiert intern dieselbe Tag-Konfiguration, die bisher manuell gepflegt wurde. Das Ergebnis ist weniger Konfigurationsaufwand, weniger Fehlerquellen durch Desynchronisation zwischen YAML und PHP und sauberere Code-Basis.
Der größte praktische Vorteil ist die Kolokalität: Wer die Listener-Klasse öffnet, sieht sofort alle Events, Methoden und Prioritäten, ohne zwischen mehreren Dateien wechseln zu müssen. bin/console debug:event-dispatcher bleibt das zentrale Debugging-Tool für die Übersicht über alle registrierten Listener. Neue Symfony-Projekte sollten #[AsEventListener] als Standard für alle Event Listener einsetzen; bestehende Projekte können die Migration schrittweise angehen, da beide Ansätze in einem Projekt koexistieren.
#[AsEventListener] in Symfony — Das Wichtigste auf einen Blick
Kein YAML mehr
#[AsEventListener] direkt auf Klasse oder Methode — Symfony registriert den Listener automatisch bei aktivem autoconfigure: true.
Typsichere Events
Event-Name als FQCN oder KernelEvents-Konstante — IDE-Unterstützung und Refactoring-Sicherheit statt Magic-Strings.
Prioritäten
priority: im Attribut steuert Ausführungsreihenfolge. bin/console debug:event-dispatcher zeigt alle Listener sortiert.
Wiederholbar
Mehrere #[AsEventListener] auf einer Klasse oder Methode möglich — eine Klasse kann so viele Events abonnieren wie nötig.