Laravel Event System and Broadcasts
The event system is Laravel's "communication network"—the sender broadcasts a message, and the receivers each handle it on their own; the sender doesn't need to know who is listening.
1. What You'll Learn
- Events and Listeners: EventServiceProvider Registration and Auto-Discovery
- Event Scheduling: event() vs Event::dispatch()
- Broadcasting mechanism: Redis Pub/Sub + Laravel Echo + Pusher
- Channel Type: Public/Private/Presence Channel
- Front-end reception: Laravel Echo + WebSocket real-time notifications
2. A Product Manager's True Story
(1) Pain Point: You have to manually refresh the page to see changes in order status
Alice manages orders in the ShopMetrics backend—she can't see them right after customers place them and has to manually refresh the page every 5 minutes. Bob has it even worse; he manages three stores at the same time, can't keep up with the refreshing, and missed five urgent orders. Charlie said, "It's 2024 and we're still manually refreshing? Have you heard of WebSocket real-time notifications?"
(2) Solutions for Event Broadcasting
Laravel Event Broadcasting—An event is triggered when an order is created; the server pushes the event to the front end via WebSocket, and Alice's page automatically refreshes with zero latency.
// Order placed → event dispatched → WebSocket broadcast
event(new OrderPlaced($order));
// Alice's browser receives notification in real-time
(3) Revenue
With Alice's real-time notifications, new orders appear on the dashboard within 0.5 seconds, so you'll never miss an order again.
3. Events and Listeners
(1) Creating Events and Listeners
php artisan make:event OrderPlaced
php artisan make:listener SendOrderNotification --event=OrderPlaced
php artisan make:listener UpdateShopRevenue --event=OrderPlaced
php artisan make:listener SendOrderWebhook --event=OrderPlaced
(2) Event Class
// app/Events/OrderPlaced.php
class OrderPlaced implements ShouldBroadcast
{
use Dispatchable, InteractsWithSockets, SerializesModels;
public function __construct(
public Order $order,
) {}
public function broadcastOn(): array
{
return [
new PrivateChannel('tenant.' . $this->order->tenant_id),
new PrivateChannel('shop.' . $this->order->shop_id),
];
}
public function broadcastWith(): array
{
return [
'order_id' => $this->order->id,
'order_number' => $this->order->order_number,
'total' => (float) $this->order->total,
'shop_name' => $this->order->shop->name,
'customer_name' => $this->order->user->name,
];
}
public function broadcastAs(): string
{
return 'order.placed';
}
}
(3) Listener Classes
// app/Listeners/SendOrderNotification.php
class SendOrderNotification
{
public function handle(OrderPlaced $event): void
{
$order = $event->order;
// Send email notification to shop owner
$order->shop->tenant->users()
->where('role', 'tenant_owner')
->each(fn ($user) => $user->notify(new OrderCreatedNotification($order)));
}
}
// app/Listeners/UpdateShopRevenue.php
class UpdateShopRevenue
{
public function handle(OrderPlaced $event): void
{
$event->order->shop->increment('revenue', $event->order->total);
}
}
(4) Register an event listener
// app/Providers/EventServiceProvider.php
protected $listen = [
OrderPlaced::class => [
SendOrderNotification::class,
UpdateShopRevenue::class,
SendOrderWebhook::class,
],
OrderStatusChanged::class => [
SendStatusChangeNotification::class,
],
];
(1) ▶ Example: ShopMetrics Order Event Trigger
// app/Http/Controllers/OrderController.php
public function store(StoreOrderRequest $request): RedirectResponse
{
$order = DB::transaction(function () use ($request) {
$order = Order::create($request->validated());
foreach ($request->items as $item) {
$order->items()->create($item);
}
$order->updateTotal();
return $order;
});
// Dispatch event — triggers all listeners + broadcast
event(new OrderPlaced($order));
return redirect()->route('orders.show', $order)
->with('success', 'Order placed!');
}
Output:
// Execution Successful
4. Event Scheduling
(1) Scheduling Method
// Method 1: event() helper (recommended)
event(new OrderPlaced($order));
// Method 2: Event facade
Event::dispatch(new OrderPlaced($order));
// Method 3: Static dispatch on event class
OrderPlaced::dispatch($order);
(2) Synchronous vs. Asynchronous Listeners
// Sync listener — runs in request cycle
class UpdateShopRevenue implements ShouldHandleEventsAfterCommit
{
public function handle(OrderPlaced $event): void
{
$event->order->shop->increment('revenue', $event->order->total);
}
}
// Async listener — pushed to queue
class SendOrderWebhook implements ShouldQueue
{
use InteractsWithQueue;
public int $tries = 3;
public int $backoff = 30;
public function handle(OrderPlaced $event): void
{
Http::post($event->order->shop->webhook_url, [
'event' => 'order.placed',
'data' => new OrderResource($event->order),
]);
}
}
| Type | Interface | Implementation Method | Suitable Scenarios |
|---|---|---|---|
| Synchronous | None | Sequential execution within the request | Update database |
| Asynchronous | ShouldQueue | Enqueue for asynchronous execution | Send email/Webhook |
| Post-Transaction | AfterCommit | Executed after the transaction is committed | Depends on persisted data |
(1) ▶ Example: ShopMetrics Events and Listener Registration
// app/Providers/EventServiceProvider.php
class EventServiceProvider extends ServiceProvider
{
protected $listen = [
// Order events
OrderPlaced::class => [
UpdateShopRevenue::class, // sync — update stats
SendOrderNotification::class, // async — send email
SendOrderWebhook::class, // async — call webhook
],
OrderStatusChanged::class => [
SendStatusNotification::class, // async
UpdateAnalyticsCache::class, // sync — clear cache
],
SubscriptionCreated::class => [
SendWelcomeEmail::class, // async
ProvisionTenantResources::class, // async
],
];
}
Output:
// Execution Successful
5. Broadcast Mechanism
(1) Broadcast Architecture
sequenceDiagram
participant S as Server
participant E as Event
participant B as Broadcaster
participant WS as WebSocket Server
participant C as Client (Echo)
S->>E: event(new OrderPlaced($order))
E->>B: broadcastOn() → channels
B->>WS: Publish to Redis channel
WS->>C: Push via WebSocket
C->>C: Echo receives & updates UI
(2) Broadcast Configuration
# .env
BROADCAST_CONNECTION=redis
QUEUE_CONNECTION=redis
# Install dependencies
composer require pusher/pusher-php-server
# Or for Redis:
# predis/predis already installed
// config/broadcasting.php
'default' => env('BROADCAST_CONNECTION', 'redis'),
'connections' => [
'pusher' => [
'driver' => 'pusher',
'key' => env('PUSHER_APP_KEY'),
'secret' => env('PUSHER_APP_SECRET'),
'app_id' => env('PUSHER_APP_ID'),
],
'redis' => [
'driver' => 'redis',
'connection' => 'default',
],
],
(3) Comparison of Broadcast Drivers
| Drivers | Services | Self-hosted | Performance | Cost |
|---|---|---|---|---|
| Pusher | Pusher Cloud | ❌ | High | Paid |
| Redis | Redis + Laravel Echo Server | ✅ | High | Free |
| Ably | Ably Cloud | ❌ | High | Paid |
| Log | Log (for development) | ✅ | — | Free |
(1) ▶ Example: ShopMetrics Broadcast Event Definitions
// app/Events/OrderStatusChanged.php
class OrderStatusChanged implements ShouldBroadcast
{
use Dispatchable, InteractsWithSockets, SerializesModels;
public function __construct(
public Order $order,
public string $oldStatus,
public string $newStatus,
) {}
public function broadcastOn(): array
{
return [
new PrivateChannel('tenant.' . $this->order->tenant_id),
];
}
public function broadcastWith(): array
{
return [
'order_id' => $this->order->id,
'order_number' => $this->order->order_number,
'old_status' => $this->oldStatus,
'new_status' => $this->newStatus,
'updated_at' => $this->order->updated_at->toISOString(),
];
}
public function broadcastAs(): string
{
return 'order.status_changed';
}
}
Output:
// Execution Successful
6. Channel Types and Authorization
(1) Three Types of Channels
| Channel | Prefix | Visibility | Purpose |
|---|---|---|---|
| Public | channel- |
Everyone | Announcements, Site-wide Notices |
| Private | private- |
Authorized User | Tenant/User-Exclusive |
| Presence | presence- |
Authorization + Online List | Collaboration, Chat |
(2) Channel Authorization
// routes/channels.php
use Illuminate\Support\Facades\Broadcast;
// Private channel — only tenant members can listen
Broadcast::channel('tenant.{tenantId}', function ($user, $tenantId) {
return $user->tenant_id === (int) $tenantId;
});
// Private channel — only shop owner/analyst
Broadcast::channel('shop.{shopId}', function ($user, $shopId) {
$shop = Shop::find($shopId);
return $shop && $user->tenant_id === $shop->tenant_id;
});
// Presence channel — who's online
Broadcast::channel('shop.dashboard.{shopId}', function ($user, $shopId) {
if ($user->tenant_id === Shop::find($shopId)?->tenant_id) {
return ['id' => $user->id, 'name' => $user->name, 'role' => $user->role];
}
});
(1) ▶ Example: ShopMetrics Channel Authorization
// routes/channels.php
Broadcast::channel('tenant.{tenantId}', function ($user, $tenantId) {
return $user->tenant_id === (int) $tenantId
&& $user->tenant->status === 'active';
});
Broadcast::channel('shop.{shopId}', function ($user, $shopId) {
$shop = Shop::find($shopId);
if (!$shop || $user->tenant_id !== $shop->tenant_id) {
return false;
}
return ['id' => $user->id, 'name' => $user->name];
});
Broadcast::channel('notifications.{userId}', function ($user, $userId) {
return (int) $user->id === (int) $userId;
});
Output:
// Execution Successful
7. Front-End Reception
(1) Install Laravel Echo
npm install laravel-echo pusher-js
# Or for Redis:
npm install laravel-echo-connector socket.io-client
(2) Configure Echo
// resources/js/app.js
import Echo from 'laravel-echo';
import Pusher from 'pusher-js';
window.Pusher = Pusher;
window.Echo = new Echo({
broadcaster: 'pusher',
key: import.meta.env.VITE_PUSHER_APP_KEY,
cluster: import.meta.env.VITE_PUSHER_APP_CLUSTER ?? 'mt1',
wsHost: import.meta.env.VITE_PUSHER_HOST,
wsPort: import.meta.env.VITE_PUSHER_PORT ?? 6001,
forceTLS: false,
enabledTransports: ['ws'],
});
(3) Listening for Events
// Listen on private channel — tenant-specific events
window.Echo.private(`tenant.${tenantId}`)
.listen('.order.placed', (e) => {
showToast(`New order: ${e.order_number} — $${e.total}`);
updateOrdersList(e);
})
.listen('.order.status_changed', (e) => {
updateOrderStatus(e.order_id, e.new_status);
});
// Listen on presence channel — see who's online
window.Echo.join(`shop.dashboard.${shopId}`)
.here((users) => {
updateOnlineUsers(users);
})
.joining((user) => {
addOnlineUser(user);
})
.leaving((user) => {
removeOnlineUser(user);
});
(1) ▶ Example: ShopMetrics Real-Time Dashboard
<!-- resources/views/dashboard/index.blade.php -->
<script>
const tenantId = {{ auth()->user()->tenant_id }};
// Initialize Echo
window.Echo.private(`tenant.${tenantId}`)
.listen('.order.placed', (event) => {
// Update stats
const stats = document.getElementById('stats');
const orderCount = stats.querySelector('.order-count');
orderCount.textContent = parseInt(orderCount.textContent) + 1;
// Add to recent orders
const list = document.getElementById('recent-orders');
list.insertAdjacentHTML('afterbegin', `
<tr class="bg-green-50">
<td>${event.order_number}</td>
<td>${event.shop_name}</td>
<td>$${event.total.toFixed(2)}</td>
<td><span class="badge-blue">New</span></td>
</tr>
`);
// Show notification
showNotification(`New order from ${event.customer_name}: $${event.total}`);
})
.listen('.order.status_changed', (event) => {
const row = document.querySelector(`[data-order="${event.order_id}"]`);
if (row) {
row.querySelector('.status-badge').textContent = event.new_status;
row.querySelector('.status-badge').className = `status-badge badge-${event.new_status}`;
}
});
</script>
Output:
// Execution Successful
8. Comprehensive Example: ShopMetrics Real-Time Notification System
// ============================================
// Comprehensive: ShopMetrics Real-time Notifications
// Covers: events, listeners, broadcast, channels, Echo
// ============================================
// app/Events/OrderPlaced.php
class OrderPlaced implements ShouldBroadcast
{
use Dispatchable, InteractsWithSockets, SerializesModels;
public function __construct(public Order $order) {}
public function broadcastOn(): array
{
return [
new PrivateChannel('tenant.' . $this->order->tenant_id),
new PrivateChannel('shop.' . $this->order->shop_id),
];
}
public function broadcastWith(): array
{
$this->order->load('shop', 'user');
return [
'order_id' => $this->order->id,
'order_number' => $this->order->order_number,
'total' => (float) $this->order->total,
'status' => $this->order->status,
'shop' => ['id' => $this->order->shop->id, 'name' => $this->order->shop->name],
'customer' => ['id' => $this->order->user->id, 'name' => $this->order->user->name],
'created_at' => $this->order->created_at->toISOString(),
];
}
public function broadcastAs(): string { return 'order.placed'; }
}
// app/Events/OrderStatusChanged.php
class OrderStatusChanged implements ShouldBroadcast
{
use Dispatchable, InteractsWithSockets, SerializesModels;
public function __construct(
public Order $order,
public string $oldStatus,
public string $newStatus,
) {}
public function broadcastOn(): array
{
return [new PrivateChannel('tenant.' . $this->order->tenant_id)];
}
public function broadcastWith(): array
{
return [
'order_id' => $this->order->id,
'order_number' => $this->order->order_number,
'old_status' => $this->oldStatus,
'new_status' => $this->newStatus,
];
}
public function broadcastAs(): string { return 'order.status_changed'; }
}
// Triggering in service class
class OrderService
{
public function place(array $data): Order
{
$order = DB::transaction(function () use ($data) {
$order = Order::create($data);
// ... create items, calculate total
return $order;
});
event(new OrderPlaced($order));
return $order;
}
public function changeStatus(Order $order, string $newStatus): Order
{
$oldStatus = $order->status;
$order->update(['status' => $newStatus]);
event(new OrderStatusChanged($order, $oldStatus, $newStatus));
return $order;
}
}
❓ FAQ
broadcastWith() to send only the necessary fields (ID + key information); once the front end receives it, it can then use AJAX to fetch the complete data. Avoid embedding a large number of related resources within the broadcast data.Event::fake() to simulate event dispatch, and assert that the event is dispatched: Event::assertDispatched(OrderPlaced::class). Write separate unit tests for the listeners.📖 Summary
- Decouples event senders and receivers; new features can be added simply by adding a listener
- The ShouldQueue listener executes asynchronously and does not block requests
- The ShouldBroadcast interface allows events to be broadcast via WebSocket
- Private Channels require authorization; Presence Channels also display a list of users currently online
- The Laravel Echo front-end library centrally handles WebSocket connections and event listening
- broadcastWith() controls the amount of data broadcasted, transmitting only the necessary fields
📝 Exercises
-
Basic Exercise (⭐): Create the
OrderPlacedevent and theSendOrderNotificationlistener, which is triggered when an order is created. UseEvent::fake()to write a test to verify that the event is dispatched. -
Advanced Exercise (⭐⭐): Implement the
ShouldBroadcastbroadcast for theOrderPlacedevent, configure the Redis broadcast driver, and use Laravel Echo in the frontend to listen to theprivate-tenant.{id}channel to display real-time notifications for new orders. -
Challenge (⭐⭐⭐): Implement a multi-user collaborative dashboard using the Presence Channel—when multiple users view the same store data simultaneously, display a list of online users; when one user modifies the data, other users see the changes in real time (broadcasting order status changes).



