Sobald in einer Anwendung eine einzige Aktion mehrere Reaktionen auslösen soll, stellt sich die Frage nach sauberer Kopplung zwischen den beteiligten Objekten. Das Observer Pattern ist die klassische Antwort darauf und gehört zu den am häufigsten genutzten Entwurfsmustern in der PHP-Welt. Dieser Artikel führt Schritt für Schritt durch Subject, Observer und die Methoden attach, detach und notify und ordnet das Muster am Ende in den heutigen PSR-14-Standard ein.
Was ist das PHP Observer Pattern?
Wer in einer PHP-Anwendung an einer Stelle eine Änderung auslöst, die mehrere andere Komponenten interessiert (eine neue Bestellung soll eine E-Mail, eine Statistik-Aktualisierung und einen Webhook auslösen), kann diese Empfänger entweder fest verdrahten oder lose koppeln. Genau die lose Kopplung ist das Versprechen vom Observer Pattern in PHP: Ein Subject sendet eine Benachrichtigung an alle registrierten Observer, ohne sie im Detail zu kennen, und sobald sich der Zustand ändert, werden alle Listener informiert.

Bevor Subject- und Observer-Interfaces im Detail folgen, ein kurzer Blick auf die historische Einordnung und den heutigen PSR-14-Standard.
Das Observer Design Pattern ist eines der am häufigsten genutzten Verhaltensmuster der Gang of Four und liegt unter anderem dem WordPress-Hook-System, dem Symfony EventDispatcher und vielen Domain-Event-Implementierungen zugrunde. In modernen PHP-Codebasen gehört es zu den meistgenutzten Designmustern überhaupt, und wer es einmal in der eigenen Anwendung sauber eingesetzt hat, will es nicht mehr missen. Dieses Tutorial führt das Pattern Schritt für Schritt ein. Wir beginnen mit dem Einsatz als Designmuster in eigenen Interfaces, gehen kurz auf die SPL-Variante ein und stellen das Ganze dem PSR-14-Standard für Event-Dispatching gegenüber.
Subject und Observer als Interfaces im PHP Observer Pattern
Den Kern vom PHP Observer Pattern bilden zwei Interfaces. Das Subject sammelt Observer und benachrichtigt sie. Der Observer reagiert auf die Benachrichtigung. Ein eigenes Interfaces-Paar bringt mehr Flexibilität beim Typing als die SPL-Varianten.
<?php
interface Observer
{
public function update(Subject $subject): void;
}
interface Subject
{
public function attach(Observer $observer): void;
public function detach(Observer $observer): void;
public function notify(): void;
}
Die update()-Methode bekommt das Subject übergeben, damit der Observer den geänderten Wert beim Subject abrufen kann (Pull-Variante). Alternativ könnte man bei notify() das geänderte Datum direkt mitgeben (Push-Variante). In der Praxis ist die Pull-Variante flexibler, weil sich das Subject erweitern lässt, ohne alle Observer-Signaturen anzufassen.
Klassische Implementierung mit attach, detach und notify
Ein Subject braucht drei Methoden: attach() registriert einen Observer, detach() entfernt ihn wieder, und notify() informiert alle registrierten Observer. Die Funktion spl_object_id() liefert eine eindeutige Id pro Objektinstanz und sorgt dafür, dass derselbe Observer nicht versehentlich mehrfach registriert wird.
<?php
final class TemperatureSensor implements Subject
{
/** @var Observer[] */
private array $observers = [];
private float $temperature = 0.0;
public function attach(Observer $observer): void
{
$this->observers[spl_object_id($observer)] = $observer;
}
public function detach(Observer $observer): void
{
unset($this->observers[spl_object_id($observer)]);
}
public function notify(): void
{
foreach ($this->observers as $observer) {
$observer->update($this);
}
}
public function setTemperature(float $value): void
{
$this->temperature = $value;
$this->notify();
}
public function getTemperature(): float
{
return $this->temperature;
}
}
Sobald setTemperature() aufgerufen wird, ruft das Subject intern notify() und alle registrierten Observer reagieren. Die Observer wissen nichts voneinander, und das Subject muss seine Empfänger nicht im Detail kennen.
Anwendungsfall Sensor mit mehreren Empfängern
Mit dem Subject im Rücken lassen sich beliebige Observer schreiben. Ein Display zeigt den aktuellen Wert an, ein Logger schreibt ihn in die Datei, ein Alarm reagiert nur auf Werte oberhalb einer Schwelle. Dank instanceof können Observer prüfen, ob das Subject von einem für sie relevanten Typ ist.
<?php
final class DisplayObserver implements Observer
{
public function update(Subject $subject): void
{
if ($subject instanceof TemperatureSensor) {
echo 'Display: ' . $subject->getTemperature() . " Grad" . PHP_EOL;
}
}
}
final class AlarmObserver implements Observer
{
public function update(Subject $subject): void
{
if ($subject instanceof TemperatureSensor && $subject->getTemperature() > 80.0) {
echo "ALARM: Temperatur zu hoch!" . PHP_EOL;
}
}
}
$sensor = new TemperatureSensor();
$sensor->attach(new DisplayObserver());
$sensor->attach(new AlarmObserver());
$sensor->setTemperature(85.0);
/* Display: 85 Grad
ALARM: Temperatur zu hoch! */
Wer einen weiteren Observer ergänzen möchte (etwa einen Webhook, der den Wert an eine externe API sendet), schreibt eine neue Klasse, die Observer implementiert, und ruft attach(). Der Sensor-Code bleibt völlig unangetastet.
flowchart TD
A[Subject aendert Zustand] --> B[notify]
B --> C[Observer 1 Display]
B --> D[Observer 2 Logger]
B --> E[Observer 3 Alarm]
SplSubject und SplObserver als SPL-Variante vom PHP Observer Pattern
PHP bringt mit SplSubject, SplObserver und SplObjectStorage eine Standard-Implementierung vom PHP Observer Pattern mit. Die Interfaces sind seit PHP 5.1 verfügbar und bis heute nutzbar. In modernen Codebasen sieht man sie nur noch selten, weil eigene Interfaces präziseres Typing erlauben und die Methoden-Signaturen nicht durch die generische SPL-API gebunden sind.
<?php
final class SplSensor implements SplSubject
{
private SplObjectStorage $observers;
private float $temperature = 0.0;
public function __construct()
{
$this->observers = new SplObjectStorage();
}
public function attach(SplObserver $observer): void
{
$this->observers->attach($observer);
}
public function detach(SplObserver $observer): void
{
$this->observers->detach($observer);
}
public function notify(): void
{
foreach ($this->observers as $o) {
$o->update($this);
}
}
}
SplObjectStorage ist als Sammlung speziell für Objekte gedacht und übernimmt die Doppel-Registrierungs-Prüfung automatisch. Wer die SPL-Variante nimmt, spart sich den Umgang mit spl_object_id, gibt im Gegenzug aber die freie Wahl der Methoden-Signaturen auf.
Vergleich zu PSR-14 EventDispatcher
In größeren Anwendungen reicht das klassische Subject-Observer-Schema oft nicht mehr aus. Sobald viele unterschiedliche Events auftreten, würde jede Subject-Klasse eine eigene Observer-Verwaltung brauchen. Genau hier setzt PSR-14 an: ein zentraler EventDispatcher kümmert sich um die Verteilung, und ein ListenerProvider liefert pro Event-Typ die zuständigen Listener.
<?php
/* PSR-14: ein generischer Event-Dispatcher mit einem
ListenerProvider, der zu jedem Event-Objekt die passenden
Listener liefert. Vorteil: das Subject muss nicht jedes
Listener-Set selbst halten, das uebernimmt der Dispatcher zentral */
final class TemperatureChanged
{
public function __construct(public readonly float $value) {}
}
interface EventDispatcher
{
public function dispatch(object $event): object;
}
/* Das Subject ruft nur noch:
$dispatcher->dispatch(new TemperatureChanged(85.0));
und alle registrierten Listener werden ausgeloest */
Wer Symfony, Laravel oder Slim nutzt, hat in der Regel bereits einen PSR-14-Dispatcher zur Verfügung. Für kleine Anwendungen oder Plugins ist das klassische PHP Observer Pattern dagegen völlig ausreichend und braucht keine externe Abhängigkeit. Die Frage, ob ein eigenes Subject-Observer reicht oder ob ein zentraler Dispatcher die bessere Wahl ist, lässt sich grob an drei Punkten festmachen: Anzahl der Event-Typen (mehr als 5 spricht für Dispatcher), Anzahl der Listener pro Event (mehr als 3 spricht für Dispatcher) und ob mehrere unabhängige Module Listener registrieren sollen (klares Argument für Dispatcher).
Ein weiterer Vorteil von PSR-14 ist die Idee der stoppable events. Ein Event-Objekt kann signalisieren, dass keine weiteren Listener mehr ausgeführt werden sollen. Das lässt sich im klassischen PHP Observer Pattern zwar nachbauen, ist aber keine Standard-Funktion und erzeugt zusätzliche Komplexität im Subject. Wer solche Anforderungen hat, ist mit einer fertigen PSR-14-Bibliothek besser bedient.
Push-Variante mit Event-Objekt
Statt das Subject im update() als Ganzes zu übergeben (Pull-Variante), kann ein dediziertes Event-Objekt mitgereicht werden. Die Observer bekommen damit nur die Daten, die wirklich relevant sind, und das Subject bleibt gekapselt. Diese Push-Variante ist konzeptionell näher an PSR-14 und fühlt sich vertraut an, sobald später ein Dispatcher dazukommt.
<?php
final class TemperatureChangedEvent
{
public function __construct(public readonly float $value) {}
}
interface PushObserver
{
public function onTemperatureChanged(TemperatureChangedEvent $event): void;
}
final class PushSensor
{
/** @var PushObserver[] */
private array $observers = [];
public function attach(PushObserver $observer): void
{
$this->observers[spl_object_id($observer)] = $observer;
}
public function setTemperature(float $value): void
{
$event = new TemperatureChangedEvent($value);
foreach ($this->observers as $observer) {
$observer->onTemperatureChanged($event);
}
}
}
Der Vorteil: Observer müssen keine instanceof-Prüfung mehr machen, weil das Event seine Daten direkt mitbringt. Der Nachteil: Wer das Event später um ein neues Feld erweitert, muss alle Observer-Signaturen mitziehen oder ein neues Event-Objekt einführen. Für kleine, klar abgegrenzte Subjects ist Push die saubere Wahl, für breite Generic-Subjects bleibt Pull flexibler.
Priorisierte Observer
Wenn die Reihenfolge der Benachrichtigung wichtig ist (Logger zuerst, Webhook zuletzt), reicht das einfache Array nicht mehr aus. Eine kleine Erweiterung um eine Priorität macht das Pattern sortierbar, ohne den Aufrufer-Code zu ändern.
<?php
final class PrioritySensor implements Subject
{
/** @var array<int, array{observer: Observer, priority: int}> */
private array $observers = [];
public function attach(Observer $observer, int $priority = 0): void
{
$this->observers[spl_object_id($observer)] = [
'observer' => $observer,
'priority' => $priority,
];
}
public function detach(Observer $observer): void
{
unset($this->observers[spl_object_id($observer)]);
}
public function notify(): void
{
$sortiert = $this->observers;
/* Hoehere Prioritaet zuerst */
usort($sortiert, fn($a, $b) => $b['priority'] <=> $a['priority']);
foreach ($sortiert as $eintrag) {
$eintrag['observer']->update($this);
}
}
}
/* $sensor->attach(new LoggerObserver(), 100); // laeuft als erstes
$sensor->attach(new WebhookObserver(), 0); // laeuft danach */
Die Sortierung passiert bei jedem notify(). Für Subjects mit hohem Notify-Aufkommen lohnt sich Caching der sortierten Liste, sobald attach/detach selten passieren. PSR-14-Implementierungen wie der Symfony EventDispatcher liefern diese Mechanik im ListenerProvider fertig mit, das obige Beispiel zeigt das Grundprinzip in 20 Zeilen.
Stolperfallen und Best Practices für das PHP Observer Pattern
Im Alltag lauern beim PHP Observer Pattern drei typische Fallen. Erstens: Wer Observer per array[] = $observer registriert, kann denselben Observer mehrfach hinzufügen. Mit spl_object_id() als Schlüssel oder SplObjectStorage ist das ausgeschlossen.
Zweitens blockieren langsame Observer den Notify-Loop. Wenn ein Observer eine externe API ruft, die zwei Sekunden braucht, wartet das Subject zwei Sekunden. Für solche Fälle ist eine Queue (etwa per enqueue mit asynchronen Workern) die bessere Lösung. Drittens entstehen in Long-Running-Prozessen Memory-Leaks, wenn Observer nicht abgemeldet werden. In klassischen PHP-Web-Requests spielt das keine Rolle, weil der Prozess am Ende des Requests endet, in CLI-Tools mit Schleifen oder in Workern muss man darauf achten.
Eine vierte Stelle, die im Code-Review oft auftaucht, ist die Reihenfolge der Benachrichtigungen. Das PHP Observer Pattern sieht keine Garantie über die Reihenfolge der Observer vor. Wer auf eine bestimmte Reihenfolge angewiesen ist (zum Beispiel: Logger immer als erstes, Webhook immer als letztes), sollte das nicht implizit über die Reihenfolge des attach()-Aufrufs steuern, sondern explizit über Prioritäten. Genau das bietet PSR-14 mit dem ListenerProvider, in eigenen Implementierungen lässt sich eine sortierte Observer-Liste leicht nachrüsten.
Eine letzte Empfehlung betrifft die Sichtbarkeit der Observer im Code. Wer ein Subject in einem großen System einsetzt, verteilt die Logik auf viele Klassen, ohne dass auf den ersten Blick erkennbar ist, welche Observer am Ende reagieren. Das ist die Kehrseite der losen Kopplung. In modernen IDEs hilft eine gute Namenskonvention, etwa Suffix Listener oder Subscriber und ein zentraler Registrierungspunkt im Bootstrap. So bleibt die Übersicht erhalten, und neue Teammitglieder finden schneller heraus, was bei einem Event tatsächlich passiert.
Fazit zum PHP Observer Pattern
Das PHP Observer Pattern ist ein bewährtes Werkzeug für lose gekoppelte Reaktion auf Zustandsänderungen. Wer ein Subject mit attach, detach und notify und ein paar Observer braucht, ist mit eigenen Interfaces oder den SPL-Varianten gut bedient. Sobald viele Events durch das System fließen oder externe Listener aus mehreren Modulen registriert werden sollen, lohnt sich der Wechsel auf einen PSR-14 EventDispatcher. Das Pattern bleibt aber die Grundlage und ist auch heute noch der schnellste Weg, eine 1:n-Benachrichtigung in PHP sauber zu implementieren.