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.