Eine Funktion ruft direkt die nächste auf, die wiederum die übernächste – und nach drei Jahren sieht die Bestellabschluss-Funktion aus wie ein Telefonbuch. E-Mail senden, Lagerbestand aktualisieren, Protokoll schreiben, Rechnung erstellen, CRM benachrichtigen. Alles direkt verdrahtet. Eine Änderung an einer Stelle bricht fünf andere. Das ist Tight Coupling – und es ist das häufigste strukturelle Problem in gewachsenen PHP-Systemen.
Das Problem genauer betrachtet
Tight Coupling bedeutet: Funktion A weiß zu viel über Funktion B, C und D. Sie ruft sie direkt auf, kennt ihre Signaturen, hängt von ihrer Existenz ab. Sobald ich B ändern will, muss ich A verstehen. Sobald ich D entfernen will, muss ich A anpassen. In großen Systemen bedeutet das: niemand traut sich mehr, etwas anzufassen.
Was ein Event-Dispatcher macht
Die Idee ist einfach: Statt A ruft B direkt auf, feuert A ein Event. Wer sich für dieses Event interessiert, registriert einen Listener. A weiß nichts mehr über B, C oder D. Es weiß nur: "Ich bin fertig, hier sind die relevanten Daten."
Das lässt sich in PHP mit ca. 20 Zeilen selbst implementieren, ohne externe Abhängigkeit:
class EventDispatcher
{
private array $listeners = [];
public function addListener(string $event, callable $listener): void
{
$this->listeners[$event][] = $listener;
}
public function dispatch(string $event, array $payload = []): void
{
foreach ($this->listeners[$event] ?? [] as $listener) {
$listener($payload);
}
}
}
// Globale Instanz (oder per Dependency Injection übergeben)
$dispatcher = new EventDispatcher();
Anwendungsbeispiel: Bestellabschluss
Vorher sieht die Bestellabschluss-Funktion so aus:
function bestellungAbschliessen(int $bestellId): void {
sendeBestellbestaetigung($bestellId);
aktualisiereBestand($bestellId);
schreibeProtokoll($bestellId, 'abgeschlossen');
erstelleRechnung($bestellId);
}
Nachher:
function bestellungAbschliessen(int $bestellId): void {
// Kernlogik bleibt hier
$dispatcher->dispatch('bestellung.abgeschlossen', ['id' => $bestellId]);
}
// Listener – können in beliebigen Dateien registriert werden
$dispatcher->addListener('bestellung.abgeschlossen', function(array $payload) {
sendeBestellbestaetigung($payload['id']);
});
$dispatcher->addListener('bestellung.abgeschlossen', function(array $payload) {
aktualisiereBestand($payload['id']);
});
$dispatcher->addListener('bestellung.abgeschlossen', function(array $payload) {
schreibeProtokoll($payload['id'], 'abgeschlossen');
});
Einen neuen Listener hinzufügen? Eine Zeile. Einen entfernen? Eine Zeile. Die Bestellabschluss-Funktion anfassen? Nicht mehr nötig.
Entkopplung bedeutet nicht, dass Teile nicht mehr miteinander kommunizieren. Es bedeutet, dass sie nicht mehr voneinander wissen müssen.
Schrittweise einführen
Ich beginne mit einem Bereich – zum Beispiel genau diesem Bestellabschluss. Den EventDispatcher einführen, die direkte Verdrahtung auflösen, Listener registrieren. Wenn das stabil läuft, denke ich über den nächsten Bereich nach. Nicht alles auf einmal umbauen – das erzeugt nur neue Probleme.
Wenn Composer schon da ist
Wer bereits Composer im Projekt hat, muss das Rad nicht neu erfinden: Symfonys
EventDispatcher-Komponente (symfony/event-dispatcher) ist eine vollwertige
Alternative mit deutlich mehr Features – typsichere Events, Event-Propagation stoppen,
Prioritäten für Listener. Für einfache Anwendungsfälle reicht der selbst geschriebene
Dispatcher völlig aus.
Fazit
Event-basierte Entkopplung ist keine exotische Architektur-Spielerei. Sie ist eine pragmatische Antwort auf ein konkretes Problem: Code, der in alle Richtungen verdrahtet ist und bei dem jede Änderung Überraschungen produziert. Eingeführt werden kann sie schrittweise, ohne das System auf den Kopf zu stellen – und genau das macht sie für Legacy-Projekte so geeignet.