Observer / Events
Laravel: Event::dispatch(), Listeners, Eloquent model events (created, updated)
Observer Pattern / Events
The Real-World Analogy
Picture a payment kiosk in a convenience store. A customer pays their electric bill. The moment that transaction completes, several things need to happen simultaneously: a receipt prints, the store's dashboard updates to show the new transaction count, an SMS confirmation goes to the customer's phone, and an entry gets written to the audit log for compliance.
Now, should the transaction processor -- the piece of code that actually moves the money -- know about receipt printers, dashboards, SMS gateways, and audit logs? Absolutely not. The transaction processor has one job: process the transaction. If you wire it directly to every downstream system, you end up with a class that knows about everything and breaks whenever any peripheral system changes.
Instead, imagine the kiosk has a bulletin board. When a transaction completes, the processor posts a notice: "Transaction completed -- here are the details." The receipt printer, dashboard, SMS sender, and audit logger are all watching that bulletin board. They each see the notice and independently do their thing. The transaction processor does not know who is watching, how many watchers there are, or what they do with the information. It just posts the notice and moves on.
That bulletin board is the Observer pattern. The transaction processor is the Subject (also called the Publisher or Event Emitter). The receipt printer, SMS sender, and the rest are Observers (also called Subscribers or Listeners). The subject maintains a list of observers and notifies all of them when something interesting happens.
The Anti-Pattern: Direct Coupling to Every Downstream System
Here's what code looks like without the Observer pattern. The transaction service directly calls every system that cares about the result:
class TransactionService
{
public function complete(array $transactionData): void
{
// Process the transaction
$transaction = $this->processPayment($transactionData);
// Now manually notify every dependent system
$receiptPrinter = new ReceiptPrinter();
$receiptPrinter->print($transaction);
$dashboard = new DashboardUpdater();
$dashboard->refresh($transaction);
$smsSender = new SmsSender();
$smsSender->sendConfirmation($transaction);
$auditLogger = new AuditLogger();
$auditLogger->log($transaction);
// New requirement: email notification? Add another call here.
// New requirement: loyalty points? Another call.
// This method grows forever.
}
}
Why this is a problem:
- The transaction service knows too much. It is coupled to receipt printing, dashboard logic, SMS sending, and audit logging. A change in any of those systems requires modifying the transaction service.
- Adding new reactions means modifying existing code. Want to add email notifications? You open
TransactionServiceand add more lines. This violates the Open/Closed Principle -- the class is not open for extension without modification. - Testing is painful. To test
complete(), you need to mock or stub four different services, none of which are related to the core transaction logic. - Failure cascading. If
SmsSenderthrows an exception, does the receipt still print? Does the audit log still get written? The current code would stop at the SMS failure, leaving the audit log empty. - No way to turn listeners on or off. In a test environment, you do not want to send real SMS. In staging, you might not want receipt printing. There is no mechanism to selectively enable or disable reactions.
The Fix: Use the Observer Pattern
Decouple the transaction processor from its dependents by introducing an event system:
interface ObserverInterface
{
public function handle(array $data): void;
}
class EventDispatcher
{
/** @var array<string, ObserverInterface[]> */
private array $listeners = [];
public function listen(string $event, ObserverInterface $observer): void
{
$this->listeners[$event][] = $observer;
}
public function dispatch(string $event, array $data): void
{
foreach ($this->listeners[$event] ?? [] as $observer) {
$observer->handle($data);
}
}
}
Now the transaction service just dispatches an event. It does not know or care who is listening:
class TransactionService
{
public function __construct(
private EventDispatcher $dispatcher,
) {}
public function complete(array $transactionData): void
{
$transaction = $this->processPayment($transactionData);
// One line. That's it. The service is done.
$this->dispatcher->dispatch('transaction.completed', $transaction);
}
}
Observers register themselves independently:
$dispatcher = new EventDispatcher();
$dispatcher->listen('transaction.completed', new ReceiptPrinter());
$dispatcher->listen('transaction.completed', new SmsSender());
$dispatcher->listen('transaction.completed', new AuditLogger());
$service = new TransactionService($dispatcher);
$service->complete(['amount' => 500, 'biller' => 'Meralco']);
Adding a new observer is one line. Removing one is one line. The transaction service never changes.
Subject and Observer Roles
The pattern has two core participants:
- Subject (Publisher/Emitter) -- the object that holds state and notifies observers when it changes. It maintains a list of observers and provides methods to subscribe and unsubscribe. In our example, the
EventDispatcherplays this role. - Observer (Subscriber/Listener) -- an object that wants to be notified when the subject changes. It implements a common interface (like
handle()) so the subject can notify it without knowing its concrete type.
The subject and observers communicate through a shared contract (the observer interface), not through direct references. This is what makes the pattern powerful -- the subject is completely decoupled from the observers.
Push vs. Pull Models
In the push model (shown above), the subject sends all relevant data to the observers as arguments. The observer receives everything it might need. This is simpler but can be wasteful if observers only need a subset of the data.
In the pull model, the subject sends a reference to itself, and observers pull the data they need:
public function handle(Transaction $transaction): void
{
$amount = $transaction->getAmount(); // Pull only what you need
}
Laravel uses a push model -- events carry their data as public properties.
When to Use It
Use the Observer pattern when:
- Multiple objects need to react to a single state change (transaction completed, order placed, user registered)
- You want to add new reactions without modifying the object that triggers them
- The set of dependents should be configurable or dynamic at runtime
- You need to decouple a core process from its side effects
Key takeaway: When one action triggers many reactions, do not hardcode those reactions into the action. Dispatch an event and let independent observers handle their own logic. The action stays focused, the observers stay independent, and adding new behavior is as simple as registering a new listener.
What You Learned
- The Observer pattern lets objects subscribe to events and get notified automatically when something happens
- The subject (event source) doesn't know or care what observers do — it just fires the event
- New observers can be added without modifying the subject, respecting the Open/Closed Principle
- In Laravel, this pattern powers the entire Event/Listener system —
event(),Event::listen(), and model events
Observer Pattern in Laravel
Laravel's event system is one of the most heavily used parts of the framework. It is a full implementation of the Observer pattern, with dedicated classes for events, listeners, and a dispatcher that ties them together.
Events and Listeners
In Laravel, an Event is a plain PHP class that represents something that happened. A Listener is a class that reacts to that event. The event dispatcher connects them:
// app/Events/TransactionCompleted.php
namespace App\Events;
class TransactionCompleted
{
public function __construct(
public readonly string $transactionId,
public readonly float $amount,
public readonly string $biller,
public readonly string $timestamp,
) {}
}
// app/Listeners/PrintReceipt.php
namespace App\Listeners;
use App\Events\TransactionCompleted;
class PrintReceipt
{
public function handle(TransactionCompleted $event): void
{
// The listener receives the event object with all its data
$receipt = sprintf(
"RECEIPT\nTransaction: %s\nAmount: %.2f\nBiller: %s\nDate: %s",
$event->transactionId,
$event->amount,
$event->biller,
$event->timestamp,
);
// Print or store the receipt
logger()->info('Receipt printed', ['receipt' => $receipt]);
}
}
// app/Listeners/SendSmsConfirmation.php
namespace App\Listeners;
use App\Events\TransactionCompleted;
class SendSmsConfirmation
{
public function handle(TransactionCompleted $event): void
{
// Each listener handles the same event independently
SmsGateway::send(
$event->customerPhone,
"Payment of {$event->amount} for {$event->biller} confirmed."
);
}
}
Registering Listeners
Laravel provides multiple ways to bind events to listeners. The most common is auto-discovery (Laravel 11+) or explicit registration in EventServiceProvider:
// app/Providers/EventServiceProvider.php
namespace App\Providers;
use App\Events\TransactionCompleted;
use App\Listeners\PrintReceipt;
use App\Listeners\SendSmsConfirmation;
use App\Listeners\LogToAuditTrail;
class EventServiceProvider extends ServiceProvider
{
protected $listen = [
TransactionCompleted::class => [
PrintReceipt::class,
SendSmsConfirmation::class,
LogToAuditTrail::class,
],
];
}
This is the Observer pattern registration step -- you are subscribing observers (listeners) to a subject (event). Adding a new listener is one line in this array. Removing one is deleting a line.
Dispatching Events
Events are dispatched using the Event facade or the event() helper:
// In a service or controller
use App\Events\TransactionCompleted;
class TransactionService
{
public function complete(array $data): void
{
// Process the transaction...
$transaction = $this->process($data);
// Dispatch the event -- all registered listeners fire
TransactionCompleted::dispatch(
transactionId: $transaction->id,
amount: $transaction->amount,
biller: $transaction->biller,
timestamp: now()->toIso8601String(),
);
}
}
The service does not know about receipts, SMS, or audit logs. It dispatches an event and moves on.
The Dispatcher: Illuminate\Events\Dispatcher
Under the hood, Laravel's Illuminate\Events\Dispatcher is the Subject in the Observer pattern. It maintains the mapping of events to listeners and handles notification:
use Illuminate\Support\Facades\Event;
// Register a listener at runtime
Event::listen(TransactionCompleted::class, function (TransactionCompleted $event) {
logger()->info("Transaction {$event->transactionId} completed");
});
// Check if an event has listeners
Event::hasListeners(TransactionCompleted::class); // true
// Dispatch to all listeners
Event::dispatch(new TransactionCompleted(...));
Eloquent Model Events
Eloquent models emit events automatically when their state changes. This is the Observer pattern applied to the model lifecycle:
// Available model events:
// creating, created, updating, updated, saving, saved,
// deleting, deleted, restoring, restored, retrieved
// app/Observers/TransactionObserver.php
namespace App\Observers;
use App\Models\Transaction;
class TransactionObserver
{
public function created(Transaction $transaction): void
{
// Fires after a new transaction is inserted
AuditLog::record('transaction_created', $transaction->toArray());
}
public function updated(Transaction $transaction): void
{
if ($transaction->wasChanged('status')) {
NotifyCustomer::dispatch($transaction);
}
}
public function deleted(Transaction $transaction): void
{
// Clean up related records
$transaction->receiptFiles()->delete();
}
}
Register the observer in your model or service provider:
// In the model itself (Laravel 11+)
use Illuminate\Database\Eloquent\Attributes\ObservedBy;
#[ObservedBy(TransactionObserver::class)]
class Transaction extends Model
{
// ...
}
// Or in a service provider
Transaction::observe(TransactionObserver::class);
Queued Listeners
Listeners can be queued for asynchronous processing. This is particularly useful when a listener does slow work (sending emails, calling external APIs):
use Illuminate\Contracts\Queue\ShouldQueue;
class SendSmsConfirmation implements ShouldQueue
{
public function handle(TransactionCompleted $event): void
{
// This runs asynchronously on a queue worker
SmsGateway::send($event->customerPhone, "Payment confirmed.");
}
public function failed(TransactionCompleted $event, \Throwable $exception): void
{
// Handle failure gracefully
logger()->error("SMS failed for {$event->transactionId}", [
'error' => $exception->getMessage(),
]);
}
}
By implementing ShouldQueue, the listener is automatically dispatched to the queue instead of running synchronously. The event dispatch call does not change at all -- the dispatcher handles the routing.
Event Subscribers
For classes that need to listen to multiple events, Laravel provides Event Subscribers:
namespace App\Listeners;
use Illuminate\Events\Dispatcher;
class TransactionEventSubscriber
{
public function handleCompleted(TransactionCompleted $event): void
{
// ...
}
public function handleFailed(TransactionFailed $event): void
{
// ...
}
public function subscribe(Dispatcher $events): void
{
$events->listen(TransactionCompleted::class, [self::class, 'handleCompleted']);
$events->listen(TransactionFailed::class, [self::class, 'handleFailed']);
}
}
Testing Events
Laravel makes it easy to test that events are dispatched without actually firing listeners:
use Illuminate\Support\Facades\Event;
public function test_transaction_dispatches_completed_event(): void
{
Event::fake();
$service = new TransactionService();
$service->complete(['amount' => 500]);
Event::assertDispatched(TransactionCompleted::class, function ($event) {
return $event->amount === 500.0;
});
}
Event::fake() replaces the real dispatcher with a fake that records dispatched events without firing listeners. This lets you test that the subject (your service) dispatches events correctly, without testing the observers (listeners) at the same time.
Key takeaway: Laravel's event system is the Observer pattern fully realized. Events are subjects that carry state, listeners are observers that react to state changes, and the Dispatcher manages subscriptions and notifications. Eloquent model events extend this to the data layer, giving you automatic notifications on every model lifecycle change.
This example is pre-filled and runnable. Click "Run" to see the output. Feel free to modify the code and experiment.
Complete the code below to pass all test cases. Use the hints if you get stuck.