Events & Listeners in Laravel
Laravel's event system provides a simple observer pattern implementation, allowing you to subscribe and listen for various events in your application. Events provide a great way to decouple various aspects of your application, making your code more maintainable and testable.
Event-Driven Architecture
Events allow you to build loosely coupled systems where different parts of your application can react to actions without directly depending on each other:
Benefits of Events:
- Decoupling: Components don't need to know about each other
- Maintainability: Easy to add/remove functionality without touching existing code
- Testability: Each listener can be tested independently
- Reusability: Multiple listeners can respond to the same event
- Asynchronous Processing: Listeners can be queued for background execution
Creating Events
Generate an event class using Artisan:
php artisan make:event OrderShipped
php artisan make:event UserRegistered
php artisan make:event PaymentProcessed
Example event class:
<?php
namespace App\Events;
use App\Models\Order;
use Illuminate\Broadcasting\InteractsWithSockets;
use Illuminate\Foundation\Events\Dispatchable;
use Illuminate\Queue\SerializesModels;
class OrderShipped
{
use Dispatchable, InteractsWithSockets, SerializesModels;
public $order;
public function __construct(Order $order)
{
$this->order = $order;
}
}
Event with additional context:
<?php
namespace App\Events;
use App\Models\User;
use Illuminate\Broadcasting\Channel;
use Illuminate\Broadcasting\InteractsWithSockets;
use Illuminate\Broadcasting\PresenceChannel;
use Illuminate\Contracts\Broadcasting\ShouldBroadcast;
use Illuminate\Foundation\Events\Dispatchable;
use Illuminate\Queue\SerializesModels;
class UserRegistered implements ShouldBroadcast
{
use Dispatchable, InteractsWithSockets, SerializesModels;
public $user;
public $verificationUrl;
public $source;
public function __construct(User $user, string $verificationUrl, string $source = 'web')
{
$this->user = $user;
$this->verificationUrl = $verificationUrl;
$this->source = $source;
}
// Broadcast on these channels
public function broadcastOn()
{
return new PrivateChannel('admin');
}
// Customize broadcast data
public function broadcastWith()
{
return [
'user_id' => $this->user->id,
'user_name' => $this->user->name,
'registered_at' => $this->user->created_at->toISOString(),
];
}
}
Creating Listeners
Generate listener classes using Artisan:
php artisan make:listener SendShipmentNotification --event=OrderShipped
php artisan make:listener UpdateInventory --event=OrderShipped
php artisan make:listener SendWelcomeEmail --event=UserRegistered
Example listener class:
<?php
namespace App\Listeners;
use App\Events\OrderShipped;
use App\Notifications\ShipmentNotification;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Queue\InteractsWithQueue;
class SendShipmentNotification implements ShouldQueue
{
use InteractsWithQueue;
// Number of times to attempt
public $tries = 3;
// Timeout in seconds
public $timeout = 60;
// Queue name
public $queue = 'notifications';
public function __construct()
{
//
}
public function handle(OrderShipped $event)
{
$order = $event->order;
// Send notification to customer
$order->customer->notify(
new ShipmentNotification($order)
);
// Log the shipment
\Log::info('Shipment notification sent', [
'order_id' => $order->id,
'customer_id' => $order->customer_id,
]);
}
// Handle job failure
public function failed(OrderShipped $event, $exception)
{
\Log::error('Failed to send shipment notification', [
'order_id' => $event->order->id,
'error' => $exception->getMessage(),
]);
}
}
Listener with conditional logic:
<?php
namespace App\Listeners;
use App\Events\OrderShipped;
class UpdateInventory
{
public function handle(OrderShipped $event)
{
$order = $event->order;
foreach ($order->items as $item) {
$product = $item->product;
// Decrease inventory
$product->decrement('stock', $item->quantity);
// Check if low stock
if ($product->stock < $product->low_stock_threshold) {
event(new LowStockAlert($product));
}
// Check if out of stock
if ($product->stock <= 0) {
$product->update(['is_available' => false]);
event(new OutOfStockAlert($product));
}
}
}
// Stop propagation if needed
public function shouldQueue(OrderShipped $event)
{
return $event->order->status === 'shipped';
}
}
Registering Events and Listeners
Register events and listeners in EventServiceProvider:
<?php
namespace App\Providers;
use App\Events\OrderShipped;
use App\Events\UserRegistered;
use App\Listeners\SendShipmentNotification;
use App\Listeners\UpdateInventory;
use App\Listeners\SendWelcomeEmail;
use App\Listeners\CreateUserProfile;
use Illuminate\Foundation\Support\Providers\EventServiceProvider as ServiceProvider;
class EventServiceProvider extends ServiceProvider
{
protected $listen = [
// Order events
OrderShipped::class => [
SendShipmentNotification::class,
UpdateInventory::class,
],
// User events
UserRegistered::class => [
SendWelcomeEmail::class,
CreateUserProfile::class,
],
// Laravel built-in events
'Illuminate\Auth\Events\Login' => [
'App\Listeners\LogSuccessfulLogin',
],
];
public function boot()
{
//
}
}
Tip: After adding event-listener mappings, run php artisan event:list to see all registered events and their listeners.
Dispatching Events
Dispatch events using multiple methods:
use App\Events\OrderShipped;
// Using event() helper
event(new OrderShipped($order));
// Using Event facade
Event::dispatch(new OrderShipped($order));
// Using Dispatchable trait
OrderShipped::dispatch($order);
// Dispatch if condition is true
OrderShipped::dispatchIf($order->status === 'shipped', $order);
// Dispatch unless condition is true
OrderShipped::dispatchUnless($order->status === 'cancelled', $order);
Dispatch events from models using model events:
<?php
namespace App\Models;
use App\Events\OrderShipped;
use Illuminate\Database\Eloquent\Model;
class Order extends Model
{
protected $dispatchesEvents = [
'created' => OrderCreated::class,
'updated' => OrderUpdated::class,
'deleted' => OrderDeleted::class,
];
// Or use boot method
protected static function boot()
{
parent::boot();
static::updated(function ($order) {
if ($order->isDirty('status') && $order->status === 'shipped') {
event(new OrderShipped($order));
}
});
}
}
Event Subscribers
Event subscribers allow you to subscribe to multiple events within a single class:
php artisan make:listener UserEventSubscriber
Subscriber class example:
<?php
namespace App\Listeners;
use App\Events\UserRegistered;
use App\Events\UserLoggedIn;
use App\Events\UserPasswordChanged;
use Illuminate\Events\Dispatcher;
class UserEventSubscriber
{
public function handleUserRegistration($event)
{
// Send welcome email
$event->user->sendWelcomeEmail();
// Create default preferences
$event->user->preferences()->create([
'theme' => 'light',
'notifications_enabled' => true,
]);
}
public function handleUserLogin($event)
{
// Update last login timestamp
$event->user->update([
'last_login_at' => now(),
'last_login_ip' => request()->ip(),
]);
// Log the activity
activity('user-login')
->by($event->user)
->log('User logged in');
}
public function handlePasswordChange($event)
{
// Send security notification
$event->user->notify(new PasswordChangedNotification());
// Invalidate other sessions
$event->user->invalidateOtherSessions();
}
public function subscribe(Dispatcher $events)
{
$events->listen(
UserRegistered::class,
[UserEventSubscriber::class, 'handleUserRegistration']
);
$events->listen(
UserLoggedIn::class,
[UserEventSubscriber::class, 'handleUserLogin']
);
$events->listen(
UserPasswordChanged::class,
[UserEventSubscriber::class, 'handlePasswordChange']
);
}
}
Register the subscriber in EventServiceProvider:
protected $subscribe = [
UserEventSubscriber::class,
OrderEventSubscriber::class,
];
Queued Event Listeners
Make listeners queueable for better performance:
<?php
namespace App\Listeners;
use App\Events\OrderShipped;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Queue\InteractsWithQueue;
class SendShipmentNotification implements ShouldQueue
{
use InteractsWithQueue;
public function handle(OrderShipped $event)
{
// This will run in the background
$event->order->customer->notify(
new ShipmentNotification($event->order)
);
}
// Manually determine if should queue
public function shouldQueue(OrderShipped $event)
{
return $event->order->customer->prefers_email;
}
// Specify queue connection
public function viaConnection()
{
return 'redis';
}
// Specify queue name
public function viaQueue()
{
return 'notifications';
}
}
Warning: When using queued listeners, make sure all data passed in the event is serializable. Avoid passing closures or resources (like database connections) in events.
Stopping Event Propagation
Stop event propagation by returning false from a listener:
public function handle(OrderShipped $event)
{
// Do something
if ($event->order->is_express_shipping) {
// Stop other listeners from executing
return false;
}
}
Exercise 1: Create a "ProductViewed" event and listener system. The event should track product views, update view counts, and recommend related products. Create at least 3 listeners for different concerns.
Exercise 2: Build an event subscriber for blog post events (created, updated, published, deleted). Each event should trigger appropriate actions like cache clearing, notification sending, and activity logging.
Exercise 3: Create a real-time notification system using broadcasted events. When a user receives a message, dispatch an event that broadcasts to their private channel and updates the UI in real-time.
Testing Events
Test events and listeners using Laravel's testing utilities:
use Illuminate\Support\Facades\Event;
use App\Events\OrderShipped;
public function test_order_shipped_event_is_dispatched()
{
Event::fake();
$order = Order::factory()->create(['status' => 'processing']);
$order->update(['status' => 'shipped']);
Event::assertDispatched(OrderShipped::class, function ($event) use ($order) {
return $event->order->id === $order->id;
});
}
public function test_listeners_are_called()
{
Event::fake([OrderShipped::class]);
event(new OrderShipped($order));
Event::assertListening(
OrderShipped::class,
SendShipmentNotification::class
);
}
Events and listeners provide a powerful way to build decoupled, maintainable applications. In the next lesson, we'll explore queues and job processing for handling time-consuming tasks in the background.