Observer — decoupled event notification
Concept
Observer pattern defines a one-to-many dependency: when one object (Subject/Publisher) changes state, all its dependents (Observers/Subscribers) are notified automatically. This decouples the subject from the observers — the subject doesn't know which specific classes are observing it.
Structure:
- Subject (Publisher): Maintains a list of observers. Notifies them on state change via
notify(). - Observer interface:
update($event)— called when the subject changes. - Concrete Observer: Implements the observer interface. Reacts to the notification.
Push vs Pull notification:
- Push: Subject sends data WITH the notification.
update(array $eventData). Observer gets everything it needs. - Pull: Subject sends only a reference to itself.
update(Subject $subject). Observer pulls what it needs. More flexible — observers choose what to access.
PHP implementations:
SplSubject/SplObserver: PHP's built-in interfaces.SplSubject::attach(),detach(),notify().SplObserver::update(SplSubject $subject).- Custom interfaces: More expressive, typed event objects.
Observer vs Event Dispatcher: The Observer pattern is peer-to-peer (observers know the subject). An event dispatcher (what Laravel uses) adds indirection: events are strings/objects, dispatcher decouples publisher from subscribers entirely.
Memory leak risk: If you hold a reference to the subject AND forget to detach the observer, the subject holds a reference to the observer, preventing GC. Use weak references (WeakReference) for long-lived subjects.
Code Example
<?php
// Custom Observer pattern
interface EventObserver
{
public function update(string $event, mixed $data): void;
}
class EventSubject
{
private array $observers = [];
public function subscribe(string $event, EventObserver $observer): void
{
$this->observers[$event][] = $observer;
}
public function unsubscribe(string $event, EventObserver $observer): void
{
$this->observers[$event] = array_filter(
$this->observers[$event] ?? [],
fn($o) => $o !== $observer
);
}
protected function notify(string $event, mixed $data = null): void
{
foreach ($this->observers[$event] ?? [] as $observer) {
$observer->update($event, $data);
}
// Also notify wildcard observers
foreach ($this->observers['*'] ?? [] as $observer) {
$observer->update($event, $data);
}
}
}
// Concrete Subject
class Order extends EventSubject
{
private string $status = 'pending';
public function getId(): int { return 1; }
public function fulfill(): void
{
$this->status = 'fulfilled';
$this->notify('order.fulfilled', ['order_id' => $this->getId(), 'status' => $this->status]);
}
public function cancel(string $reason): void
{
$this->status = 'cancelled';
$this->notify('order.cancelled', ['order_id' => $this->getId(), 'reason' => $reason]);
}
}
// Concrete Observers
class EmailNotificationObserver implements EventObserver
{
public function update(string $event, mixed $data): void
{
match($event) {
'order.fulfilled' => $this->sendFulfillmentEmail($data['order_id']),
'order.cancelled' => $this->sendCancellationEmail($data['order_id'], $data['reason'] ?? ''),
default => null,
};
}
private function sendFulfillmentEmail(int $id): void { echo "Email: Order {$id} fulfilled\n"; }
private function sendCancellationEmail(int $id, string $reason): void { echo "Email: Order {$id} cancelled: {$reason}\n"; }
}
class InventoryObserver implements EventObserver
{
public function update(string $event, mixed $data): void
{
if ($event === 'order.fulfilled') {
echo "Inventory: Deduct stock for order {$data['order_id']}\n";
}
}
}
// Wiring
$order = new Order();
$order->subscribe('order.fulfilled', new EmailNotificationObserver());
$order->subscribe('order.cancelled', new EmailNotificationObserver());
$order->subscribe('order.fulfilled', new InventoryObserver());
$order->fulfill();
// → Email: Order 1 fulfilled
// → Inventory: Deduct stock for order 1