0

Observer — decoupled event notification

Intermediate5 min read·eng-04-002
interviewlaravel-srccompare

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
<?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