Öffne eine beliebige gewachsene Symfony-Codebasis und grep nach onKernelResponse. Ich wette einen Kaffee, dass du mindestens einen Listener findest, der bei jedem einzelnen Request feuert, in $request->attributes nach irgendeinem halbdokumentierten Underscore-Key wühlt und in achtundneunzig Prozent der Fälle mit einem Early Return aussteigt. Dieses Muster war nie jemandes Vorstellung von gutem Design. Es war schlicht der einzige Weg, ein eigenes Controller-Attribut über den gesamten Request-Lebenszyklus hinweg nützlich zu machen, weil das Framework selbst dir keine bessere Option gab. Symfony 8.1 räumt diese Ausrede ab, und ich glaube, das verändert still und leise, wie wir Cross-Cutting-Code in unseren Apps strukturieren sollten.
Der zugrunde liegende Fehler war fast schon peinlich klein. Attribute wurden während kernel.controller aufgelöst und dann auf dem ControllerEvent-Objekt selbst geparkt. ControllerArgumentsEvent kam über eine Rückreferenz noch dran, aber spätestens beim ResponseEvent waren sie weg. Der framework-eigene CacheAttributeListener musste #[Cache]-Daten über einen privaten _cache-Key auf dem Request schmuggeln, nur um später Header setzen zu können. Wenn der Core für seine eigene API einen Workaround braucht, kopiert jeder downstream genau diesen Workaround mit eigenen Key-Namen, und exakt das ist draußen passiert.
Der Fix in 8.1 ist langweilig, und zwar im besten Sinne: Attribute liegen jetzt auf dem Request unter _controller_attributes, und da jedes Kernel-Event dieselbe Request-Instanz mitführt, sind sie an jedem Punkt des Lebenszyklus lesbar, Exception-Pfad inklusive. Es gibt sogar Aufräumlogik für den Randfall, dass setController() den Controller mitten im Flug austauscht, damit du nie Attribute eines Controllers liest, der gar nicht mehr der laufende ist. Keine Cleverness, nur State, der auf das Objekt umgezogen ist, das den ganzen Request tatsächlich überlebt. Die meisten Framework-Bugs, die ich in meiner Laufbahn gejagt habe, liefen darauf hinaus, dass State auf dem falschen Objekt lebte, deshalb schätze ich eine Release Note, die im Kern ein Eingeständnis genau davon ist.
Die zweite Änderung ist die, die den Alltagscode umkrempeln wird. Ein neuer ControllerAttributesListener dispatcht ein eigenes Sub-Event pro Attribut-Instanz, benannt aus der Kombination von Kernel-Event und FQCN des Attributs, aus kernel.response plus Cache::class wird also ein eigenes abonnierbares Event. Dein Listener registriert sich auf diesen zusammengesetzten Namen und bekommt ein ControllerAttributeEvent mit der typisierten Attribut-Instanz plus dem ursprünglichen Kernel-Event. Kein Presence-Check, kein Wühlen in Arrays, überhaupt kein Aufruf, wenn das Attribut nicht da ist. Der Container weiß zur Compile-Zeit, welche Attribut-Events Subscriber haben, es wird also nichts auf Verdacht dispatcht. Wenn du je einen Request profiliert und zugesehen hast, wie ein Dutzend Listener aufwachen, sich umschauen und wieder schlafen legen, weißt du, warum das über Ästhetik hinaus zählt.
Jetzt das ehrliche Zugeständnis, denn es gibt eins. Zusammengesetzte Event-Namen sind Strings, die mit einem Punkt zusammengeklebt werden, und stringly-typed Dispatch ist genau die Sorte Magie, die neue Teammitglieder in den Profiler starren lässt, mit der Frage, warum ihr Listener nie feuert. Auch die Lifecycle-Regeln bringen Reibung: Vor dem Controller-Lauf rufst du getAttributes() auf dem Kernel-Event auf, danach gehst du über controllerMetadata, und der falsche Aufruf für deine Phase ist ein fataler Fehler statt eines Null-Werts. Das Prioritätsschema, -10000 auf den Controller-Events und 10000 auf den späteren, ist bewusst gewählt und korrekt, aber es ist ein weiterer unsichtbarer Vertrag, von dessen Existenz du wissen musst. Wenn dein Team schon jetzt Mühe hat, dem Event-Fluss zu folgen, kommt hier eine Schicht dazu.
Ich lande trotzdem klar auf der Pro-Seite, aus einem Grund: Die Alternative war schlimmer, und wir haben alle darin gelebt. Die alte Welt hatte dieselbe Magie, nur selbstgebaut, undokumentiert und in jedem Projekt anders. Ein privater Request-Key, den irgendwer 2022 erfunden hat, ist für Neulinge deutlich feindseliger als eine Konvention auf Framework-Ebene, die debug:event-dispatcher introspizieren kann. Und der Mechanismus ist erprobt, denn #[Cache] und #[RateLimit] laufen intern jetzt exakt über diese Pipeline. Wenn der Core sein eigenes Essen isst, vertraue ich dem Rezept mehr.
Mein praktischer Rat lautet deshalb: Behandle das als Einladung zum Refactoring, nicht nur als Zeile im Changelog. Der Listener, der prüft, ob eine Route einen Audit-Log-Eintrag braucht, das middleware-artige Ding, das Tenant-Isolation durchsetzt, der Response-Decorator, der alten API-Versionen Deprecation-Header verpasst: Sie alle betteln darum, ein Attribut plus gezielter Listener zu werden. Aber ich weiß auch, wohin dieser Weg führen kann, denn ich habe Controller gesehen, die sechs Attribute tragen wie ein General seine Orden, und irgendwann heißt deklarativ eben nicht mehr lesbar. Also sag mir: Welchen generischen Kernel-Listener würdest du in deiner Codebasis als Erstes umbauen, und wie viele Attribute auf einer Action sind deine persönliche Schmerzgrenze, bevor du die Logik zurück in ganz normale Services schiebst?
Kommentare
Noch keine Kommentare — schreib den ersten.
Starte die Diskussion
Kein Konto, kein Passwort nötig — gib einfach deine E-Mail-Adresse ein, wir senden dir einen einmaligen Anmelde-Link. Beim ersten Mal bist du damit automatisch angemeldet.
Deine Bewertung wird nach der Anmeldung automatisch übernommen.
Schau in dein Postfach
Wir haben einen Anmelde-Link an … gesendet. Öffne ihn auf diesem Gerät — dieser Tab meldet dich automatisch an.
Nichts angekommen? Wirf einen Blick in den Spam-Ordner — und markiere die Mail dort als „Kein Spam“, dann landet sie künftig direkt im Postfach.