Doctrine EventSubscriber & EventListener in Symfony

Mal wieder ein Beispiel aus dem alltäglichen Wahnsinn – oder wie man Stunden mit nichts verbringen kann 😉

Doctrine kennt 2 Arten um auf Events zu reagieren: EventListener und EventSubscriber, welche ebenfalls vom SymfonyBridge Bundle in der Service Definition unterstützt werden. Die Unterschiede der beiden Implementationen sind gut in der Dokumentation beschrieben.

Was dort hingegen nicht steht, ist die Tatsache das beide Typen einzeln aggregiert werden, während der Container neu gebaut wird, was in diesem CompilerPass passiert.

Das eigentliche Problem

Man kann Prioritäten vergeben, diese sind jedoch nicht typenübergreifend gültig, sondern gelten jeweils nur innerhalb des jeweiligen Compiler Tag:

doctrine.event_subscriber
doctrine.event_listener

Will man nun einen Listener vor einem Subscriber ausführen, ist das schlicht nicht möglich weil Subscriber vor Listenern registriert werden (hier). Und intern kennt Doctrine keine Prioritäten, die Events werden nach der Reihenfolge der Registrierung an die jeweiligen Listener transportiert (hier).

Nehmen wir folgende Konfiguration:

App\Doctrine\HighPrioritySubscriber:
    class: App\Doctrine\HighPrioritySubscriber
    tags:
        - { name: doctrine.event_listener, event: prePersist, priority: 10 }

App\Doctrine\LowPrioritySubscriber:
    class: App\Doctrine\LowPrioritySubscriber
    tags:
        - { name: doctrine.event_subscriber, priority: 1 }

Entgegen des ersten Eindrucks bekommt hier immer der LowPrioritySubscriber das Event zuerst, obwohl höhere Prioritäten immer dazu führen sollten, das etwas früher benachrichtigt wird. Das Problem ist schon länger bekannt (hier und hier), aber leider nirgendwo dokumentiert wie man sich selber helfen kann. Das hole ich jetzt nach …

Die Lösung

Es gibt noch weitere Unterschiede zwischen EventListenern und EventSubscribern, aber eine der großen Unterschiede ist die Methode EventSubscriber::getSubscribedEvents(), welche in einem Rückgabe Array die Event-Namen zurückgibt auf welche die Klasse lauscht.

Man kann also mit wenig Aufwand immer die Service Definition umschreiben und den jeweils benötigten Tag verwenden um die Prioritäten vergeben zu können. Einem Listener müsste man die Methode getSubscribedEvents() spendieren um ihn in einen Listener zu verwandeln, für einen Subscriber hingegen müsste man die Event Namen in die services.yaml kopieren.

Beide Varianten sind also prinzipiell möglich, hängt vom Einsatzfall ab:

App\Doctrine\HighPrioritySubscriber:
    class: App\Doctrine\HighPrioritySubscriber
    tags:
        - { name: doctrine.event_subscriber, priority: 10 }

App\Doctrine\LowPrioritySubscriber:
    class: App\Doctrine\LowPrioritySubscriber
    tags:
        - { name: doctrine.event_subscriber, priority: 1 }

oder

App\Doctrine\HighPrioritySubscriber:
    class: App\Doctrine\HighPrioritySubscriber
    tags:
        - { name: doctrine.event_listener, event: prePersist, priority: 10 }

App\Doctrine\LowPrioritySubscriber:
    class: App\Doctrine\LowPrioritySubscriber
    tags:
        - { name: doctrine.event_listener, event: prePersist, priority: 1 }

Und wieder 2 Stunden weg, die ich viel lieber mit UnitTests verbracht hätte.