Kaum eine Programmiersprache kommt heute noch ohne Event Handling und Event Dispatcher aus. Aber was sind eigentlich Events und wie kannst Du PHP verwenden, um Events richtig zu benutzen? Genau das werde ich Dir anhand des von der PHP Framework Interop Group definierten Standards PSR-14 heute zeigen.
Was sind eigentlich Events?
Wüsstest Du auf diese Frage aus dem Stand eine einfache, verständliche Antwort? Als Programmierer gehen wir damit täglich um und benutzen Events ganz selbstverständlich. Ich persönlich musste dann doch erstmal nachsehen, ob es eine genaue Definition für so ein Event gibt.
Die PHP-FIG definiert ein Event wie folgt:
An Event is a message produced by an Emitter. It may be any arbitrary PHP object.
https://www.php-fig.org/psr/psr-14/#definitions
Ein Event ist also immer eine Nachricht, die von einer bestimmten Quelle herausgegeben wird, die im PHP Umfeld ein beliebiges Objekt sein kann.
Events eignen sich hervorragend dazu Logik zu erweitern, ohne dabei den eigentlichen Programmkern in irgend einer Art und Weise neu programmieren zu müssen.
Was brauchen wir?
Um mit Events umgehen zu können, benötigen wir nicht nur das Event selbst, sondern auch weitere Beteiligte, die mit einem Event zusammen spielen können. Damit wir genau wissen, wovon wir hier reden, gibt es für alle Beteiligten natürlich feste Definitionen.
- Event
Wie wir schon festgestellt haben, sind Events Objekte. Diese Objekte funktionieren als Datenhalter zwischen der Quelle des Events (Emitter) und allen mit diesem Event verbundenen Event Listener Instanzen. - Event Listener
Ein Event Listener kann ein beliebiges PHP Callable sein und nimmt das Event als Parameter entgegen, um die im Event enthaltenen Daten in der im Listener enthaltenen Logik verarbeiten zu können. Kurz und knapp: Der Listener reagiert auf das Event. - Event Listener Provider
Hierbei handelt es sich um eine Collection von Event Listenern, die zu bestimmten Events ausgeführt werden sollen. Der Listener Provider ist dafür verantwortlich die richtigen Listener für ein bestimmtes Event in der richtigen Reihenfolge zu liefern. - Event Dispatcher
Der Dispatcher ist eigentlich derjenige, der alle anderen Beteiligten zusammenführt. Er ist verantwortlich für das Abrufen und Ausführen von Event Listenern vom Provider aufgrund des ihn gesendeten Events. In der Regel ruft eine Quelle den Dispatcher mit einem bestimmten Event auf, der dann alle damit verbundenen Listener ausführt.
Aus Gründen der Einfachheit habe ich die Begrifflichkeiten hier einfacher erklärt. Die PHP-FIG hat alle Begrifflichkeiten nochmals sehr ausführlich in der Definition der PSR-14 „Event Dispatcher“ niedergeschrieben.
Let ’s start coding!
Jetzt, wo wir wissen, was wir alles benötigen, können wir beginnen unseren eigenen Event Dispatcher nach PSR-14 zu programmieren. Glücklicherweise gibt uns die PHP-FIG für dieses Vorhaben einige sehr nützliche Interfaces an die Hand, mit denen es uns noch leichter fällt unser Vorhaben umzusetzen.
Beginnen wir damit unser Projekt mittels Composer einzurichten.
composer init
Nachdem wir alle Daten für unser Projekt definiert haben, können wir die von der PHP-FIG definierten Interfaces in das Projekt implementieren.
composer require psr/event-dispatcher
Damit werden alle für PSR-14 „Event Dispatcher“ relevanten Interfaces in Euer Projekt implementiert.
Event Listener Provider
Wir erinnern uns daran, das der Provider das Service Objekt ist, welches uns die für ein Event registrierten Listener vor hält und sie in einer bestimmten Reihenfolge ausliefert.
<?php
declare(strict_types=1);
namespace MMNewmedia\EventDispatcher;
use ArrayObject;
use Psr\EventDispatcher as Psr;
use SplPriorityQueue;
class ListenerProvider implements Psr\ListenerProviderInterface
{
public function __construct(
protected ArrayObject $listeners = new ArrayObject(),
) {}
public function getListenersForEvent(object $event): iterable
{
$eventName = $event::class;
if ($this->listeners->offsetExists($eventName) === false) {
return new SplPriorityQueue();
}
return $this->listeners->offsetGet($eventName);
}
public function addListener(string $eventName, callable $listener, int $priority = 0): void
{
if ($this->listeners->offsetExists($eventName) === false) {
$this->listeners->offsetSet($eventName, new SplPriorityQueue());
}
$this->listeners->offsetGet($eventName)->insert($listener, $priority);
}
}
Unsere ListenerProvider
Klasse beinhaltete die durch das ListenerProviderInterface vorgegebene Methode getListenersForEvent
. Diese Methode ermittelt anhand des übergebenen Events aus der Collection aller vorhandenen Event Listener alle auszuführenden Listener in der richtigen Reihenfolge.
Die Reihenfolge der Event Listener wird durch eine Priorität festgelegt, die beim Hinzufügen der Listener mit angegeben werden kann. Ich habe mich an dieser Stelle für die SplPriorityQueue Klasse entschieden, da sie nativ schon alle benötigte Funktionalität von Haus aus mitbringt, um eine bestimmte Reihenfolge anhand von Prioritäten festlegen zu können. In vielen anderen Umsetzungen des PSR-14 Standards sieht man mehr oder weniger aufwendig gestaltete Sortierungen, auf die wir hier ganz einfach verzichten.
Event Dispatcher
Wie wir wissen, benötigen wir diese Klasse, um Event Listener für ein Event auszufüren. Auch hierfür gibt es bereits ein entsprechendes Interface, welches die Umsetzung dieser Klasse sehr einfach macht.
<?php
declare(strict_types=1);
namespace MMNewmedia\EventDispatcher;
use Psr\EventDispatcher as Psr;
class EventDispatcher implements Psr\EventDispatcherInterface
{
public function __construct(
protected readonly Psr\ListenerProviderInterface $provider = new ListenerProvider(),
) {}
public function dispatch(object $event): object
{
$listeners = clone $this->provider->getListenersForEvent($event);
foreach ($listeners as $listener) {
if ($event instanceof Psr\StoppableEventInterface && $event->isPropagationStopped() === true) {
return $event;
}
$listener($event, $this);
}
return $event;
}
}
Das PSR Interface EventDispatcherInterface
schreibt die Methode dispatch
vor, welche wir in dieser Klasse implementiert haben. Der Dispatcher bekommt die Collection aller Event Listener als Constructor Parameter übergeben. Anhand des Events, welches an die dispatch
Methode übergeben wird, ermittelt der Provider die entsprechenden Event Listener, die dann ausgeführt werden.
Wir clonen die SplPriorityQueue
Instanz an dieser Stelle. Der Grund dafür ist simpel. Eine Priority Queue ist nach dem Abarbeiten leer. Was durchaus Vorteile in der Performance hat, ist ebenso nachteilig für unseren Dispatcher. Würden wir die Queue nicht clonen, würde sie nur einmal abgearbeitet werden können. Das ergibt natürlich gar keinen Sinn, weil wir unterschiedliche Events in unterschiedlichen Quellen mehrfach ausführen können wollen.
Ein Sonderfall, welches die PSR-14 beschreibt, ist das StoppableEventInterface
. Implementiert ein Event dieses Interface, kann die Ausführung der für das Event registrierten Event Listener abgebrochen werden. Alle Event Listener, die noch hätten folgen können, werden dann nicht mehr ausgeführt.
Der Dispatcher liefert das übergebene Event nach Ausführung aller Event Listener wieder an die Quelle zurück.
Stoppable Events
Für die eben beschriebene Funktionalität von stoppable Events, definiert die PSR-14 das StoppableEventInterface
, in dem die Methode isPropagationStopped
definiert ist. Mit dieser Methode kann der Dispatcher feststellen, ob der zuvor ausgeführte Listener die Abarbeitung aller Listener unterbrochen hat.
Da wir diese Methode theoretisch in jedes Event implementieren können möchten, erstellen wir dafür ein Trait.
<?php
declare(strict_types=1);
namespace MMNewmedia\EventDispatcher;
trait StoppableEventTrait
{
protected bool $isStopped = false;
public function isPropagationStopped(): bool
{
return $this->isStopped;
}
public function stopPropagation(): void
{
$this->isStopped = true;
}
}
Die Event Klasse
Die folgende Abstraktion garantiert uns die Einbindung des StoppableEventInterface
, so dass wir der PSR-14 „Event Dispatcher“ in vollem Umfang Genüge tun.
<?php
declare(strict_types=1);
namespace MMNewmedia\EventDispatcher;
use Psr\EventDispatcher as Psr;
abstract class Event implements Psr\StoppableEventInterface
{
use StoppableEventTrait;
}
Somit können alle Events, die die Abarbeitung aller mit ihnen verbundenen Event Listener unterbrechen können sollen, dies auch tun. Natürlich muss nicht von dieser Abstraktion abgeleitet werden. Das Trait kann auch einfach ohne die Abstraktion in eine Event Klasse implementiert werden. Somit sind wir so flexibel, wie es eben nur geht.
An dieser Stelle haben wir den PSR-14 „Event Dispatcher“ Standard in vollem Umfang umgesetzt. Das war einfach, oder?
Das praktische Beispiel
Weil ich persönlich wahnsinnig gern grille, kommt das gleich gezeigte Praxisbeispiel auch nicht an diesem Thema vorbei. Wir kennen das. Wir wollen unser Steak saftig rosa medium rare vom Grill haben. Richtig schön lecker. Dafür benutze ich persönlich einen Temperaturfühler, der mir eine Push Nachricht auf mein Mobiltelefon sendet, sobald eine gewisse Garstufe erreicht ist.
Für dieses Beispiel sind Events perfekt. Das Event (die Information) ist die steigende Temperatur. Ein Event Listener soll auf die Temperatur reagieren, sobald die gewünsche Garstufe erreicht ist.
<?php
namespace MMNewmedia;
require_once __DIR__ . '/vendor/autoload.php';
use MMNewmedia\EventDispatcher;
// Garstufen
enum CookingTimes: int
{
case RAW = 38;
case RARE = 45;
case MEDIUM_RARE = 54;
case MEDIUM = 57;
case MEDIUM_WELL = 60;
case WELL_DONE = 65;
}
// Das Temperatur Event
class TemperatureEvent extends EventDispatcher\Event
{
public function __construct(
protected readonly int $temperature = 0,
) {}
public function getTemperature(): int
{
return $this->temperature;
}
}
// Event Listener, der die maximale Temperatur berücksichtigt
class TemperatureEventListener
{
public function __construct(
protected readonly CookingTimes $maxTemperature,
) {}
public function __invoke(TemperatureEvent $event): void
{
$temperature = $event->getTemperature();
if ($temperature > $this->maxTemperature->value) {
$event->stopPropagation();
}
}
}
// Ok - GO!
$provider = new EventDispatcher\ListenerProvider();
$provider->addListener(
TemperatureEvent::class,
new TemperatureEventListener(CookingTimes::MEDIUM_RARE)
);
$dispatcher = new EventDispatcher\EventDispatcher($provider);
for ($temperature = 25; $temperature <= 65; $temperature++) {
$event = $dispatcher->dispatch(new TemperatureEvent($temperature));
if ($event->isPropagationStopped() === true) {
echo "Die Temperatur ist erreicht! Nimm das sofort vom Grill!" . PHP_EOL;
break;
}
echo sprintf(
"Die Temperatur hat %s °C erreicht." . PHP_EOL,
$event->getTemperature()
);
}
Ihr werdet feststellen, dass die for-Schleife nicht bis zum Ende ausgeführt wird, sondern bei 54 °C mit der Nachricht „Die Temperatur ist erreicht! Nimm das sofort vom Grill!“ abbricht. Im Grunde genommen haben wir auf Basis des PSR-14 die einfache Logik eines Temperaturfühlers abgebildet. Nice!
Fazit
Ich bin ziemlich froh darüber, dass die PHP Framework Interop Group diese Standards definiert. Sie werden mittlerweile von vielen großen PHP Frameworks, wie z.B. Symfony, Laminas oder Slim implementiert, was dazu führt, dass ich Funktionalität innerhalb eines Frameworks sehr schnell austauschen kann, ohne dafür das halbe Framework auseinander pflücken zu müssen. Ein Event Dispatcher, welcher nach PSR-14 programmiert wurde, implementiert immer die gleichen Funktionen und kann somit mit jedem PHP Framework genutzt werden, welches diesen PSR Standard implementiert.
Es gibt allerdings auch Ausnahmen, wie das Laravel Framework, welches hier mal wieder einen ganz eigenen Weg geht. Es ist schlichtweg nicht einfach so möglich den Laravel Event Dispatcher gegen einen schnelleren, nicht dermaßen überladenen Event Dispatcher auszutauschen, weil man sich dafür entschieden hat einen ganz eigenen Weg zu gehen, den irgendwie niemand anderes geht.
Events bleiben dennoch eine perfekte Möglichkeit Funktionalität abseits von Vererbung und Co. zu erweitern. Ich kann auf Basis eines Events einfach weitere Logik in eine Applikation bringen.
Neben einem Event Dispatcher wäre das Observable Pattern eine weitere Möglichkeit auf einem ähnlich einfachen Weg die Logik einer Applikation zu erweitern.
Was haltet ihr von der hier beschriebenen PSR-14 Implementierung? Schreibt es mir in den Kommentaren. Ich freue mich auf regen Austausch.