Observer Pattern
debt(d7/e5/b5/t5)
Closest to 'only careful code review or runtime testing' (d7). The detection_hints note automated detection is 'no' and the code pattern (class directly calling methods on multiple classes after state change instead of publishing an event) requires a reviewer to recognise the architectural smell. PHPStan can catch some structural issues but cannot reliably identify missing observer abstractions; the misuse is mostly invisible until a developer reviews the design or runtime symptoms (memory leaks, crash loops, tight coupling) surface.
Closest to 'touches multiple files / significant refactor in one component' (e5). The quick_fix describes defining Subject and Observer interfaces and injecting observers, but this involves creating new interfaces, refactoring the subject class, updating all call sites that previously called methods directly, and wiring observers — a non-trivial refactor across multiple files even if contained to one component.
Closest to 'persistent productivity tax' (b5). Applies to web, cli, and queue-worker contexts. A poorly implemented observer setup (no detach, synchronous heavy observers, circular chains) creates an ongoing tax: memory leaks in long-running processes, fragile dispatch loops, and hidden coupling between components that every maintainer must understand and carefully manage. It shapes how new features hook into the system without quite defining the entire system's shape.
Closest to 'notable trap (a documented gotcha most devs eventually learn)' (t5). The misconception field identifies the canonical wrong belief: conflating the Observer pattern with an event bus. Developers familiar with pub-sub systems assume publishers and subscribers are fully decoupled via a broker, but in the Observer pattern the subject holds direct references to observers. Additionally, common_mistakes (memory leaks from undetached observers, exception propagation crashing dispatch, circular chains) are well-documented but routinely hit in practice.
Also Known As
TL;DR
Explanation
The Observer (or Event/Listener) pattern decouples the subject (event emitter) from its observers (listeners). Observers register interest with the subject; when the subject's state changes, it notifies all registered observers without knowing their concrete types. In PHP frameworks, this is implemented as event dispatchers (Symfony EventDispatcher, Laravel Events). It is ideal for cross-cutting concerns like logging, cache invalidation, and email notifications that shouldn't pollute domain logic. Avoid overuse — complex event chains are hard to trace and debug.
Common Misconception
Why It Matters
Common Mistakes
- Not removing observers when they are no longer needed — memory leaks in long-running processes.
- Observers that throw exceptions crashing the event dispatch loop — catch inside the observer or the dispatcher.
- Synchronous observers performing slow operations (email, HTTP calls) — use async dispatch for heavy work.
- Circular event chains where an observer triggers an event that re-triggers the same observer.
Avoid When
- The event chain is hard to follow — deeply nested observers triggering other observers cause debugging nightmares.
- Observers are synchronous and slow — a single slow listener blocks all subsequent listeners and the original caller.
- The order of observer execution matters and is not guaranteed — use an ordered pipeline instead.
- You only have one listener — a direct method call is simpler and equally clear.
When To Use
- Decoupling a publisher from multiple independent consumers that react to the same event.
- Plugin or extension systems where third-party code needs to hook into core events.
- Audit logging, cache invalidation, and notifications that should not be in the core business logic.
- UI frameworks and event-driven architectures where components react to state changes.
Code Examples
// Direct method calls — tight coupling:
class OrderService {
public function place(Order $o): void {
$this->save($o);
$this->emailService->sendConfirmation($o); // Coupled
$this->inventoryService->reserve($o); // Coupled
$this->analyticsService->track($o); // Coupled
}
}
// Better: dispatch OrderPlaced event; each service subscribes independently
interface EventListener {
public function handle(object $event): void;
}
class EventDispatcher {
private array $listeners = [];
public function listen(string $event, EventListener $listener): void {
$this->listeners[$event][] = $listener;
}
public function dispatch(object $event): void {
foreach ($this->listeners[$event::class] ?? [] as $listener) {
$listener->handle($event);
}
}
}
// Usage
$dispatcher->listen(OrderPlaced::class, new SendConfirmationEmail());
$dispatcher->listen(OrderPlaced::class, new UpdateInventory());
$dispatcher->dispatch(new OrderPlaced($order));