404 Not Found

404 Not Found


nginx

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


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.

PHP
// 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

BASH
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

PHP
// 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

PHP
// 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

PHP
// app/Providers/EventServiceProvider.php
protected $listen = [
    OrderPlaced::class => [
        SendOrderNotification::class,
        UpdateShopRevenue::class,
        SendOrderWebhook::class,
    ],
    OrderStatusChanged::class => [
        SendStatusChangeNotification::class,
    ],
];

(1) ▶ Example: ShopMetrics Order Event Trigger

PHP
// 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:

TEXT
// Execution Successful

4. Event Scheduling

(1) Scheduling Method

PHP
// 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

PHP
// 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

PHP
// 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:

TEXT
// Execution Successful

5. Broadcast Mechanism

(1) Broadcast Architecture

100%
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

BASH
# .env
BROADCAST_CONNECTION=redis
QUEUE_CONNECTION=redis

# Install dependencies
composer require pusher/pusher-php-server
# Or for Redis:
# predis/predis already installed
PHP
// 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

PHP
// 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:

TEXT
// 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

PHP
// 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

PHP
// 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:

TEXT
// Execution Successful

7. Front-End Reception

(1) Install Laravel Echo

BASH
npm install laravel-echo pusher-js
# Or for Redis:
npm install laravel-echo-connector socket.io-client

(2) Configure Echo

JAVASCRIPT
// 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

JAVASCRIPT
// 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

HTML
<!-- 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:

TEXT
// Execution Successful

8. Comprehensive Example: ShopMetrics Real-Time Notification System

PHP
// ============================================
// 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

Q What is the difference between events and direct calls?
A A direct call (A→B) is tightly coupled; A must be aware of B's existence. An event (A→Event→B) is loosely coupled; A does not know who is listening. New features can be added simply by adding listeners, without modifying the sender's code.
Q When should the ShouldQueue listener be used?
A Use asynchronous listeners for time-consuming operations (sending emails, triggering webhooks, generating reports); use synchronous listeners for immediate operations (updating the database, clearing the cache). Asynchronous listeners do not block requests.
Q How do I choose between Pusher and Redis for push notifications?
A Use Pusher (hosted service) for small-scale deployments or quick launches; use Redis + Laravel Echo Server (self-hosted) for large-scale deployments or when cost is a concern. Pusher has limited free quotas, while Redis server costs are fixed.
Q What should I do if the frontend isn't receiving broadcast events?
A Checklist: 1) QUEUE_CONNECTION is not set to "sync" 2) queue:work is running 3) The key/host configured in Echo is correct 4) Channel authorization returns true 5) The broadcast event implements ShouldBroadcast.
Q What should I do if the broadcast event data is too large?
A Use 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.
Q How do I test event listeners?
A Use 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


📝 Exercises

  1. Basic Exercise (⭐): Create the OrderPlaced event and the SendOrderNotification listener, which is triggered when an order is created. Use Event::fake() to write a test to verify that the event is dispatched.

  2. Advanced Exercise (⭐⭐): Implement the ShouldBroadcast broadcast for the OrderPlaced event, configure the Redis broadcast driver, and use Laravel Echo in the frontend to listen to the private-tenant.{id} channel to display real-time notifications for new orders.

  3. 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).

Web-Tutorial.com

Web-Tutorial Tech Team

A team of developers maintaining programming tutorials. Each tutorial is written and reviewed by developers with expertise in that field. We work to keep our content accurate and reliable — if you spot an issue, please let us know.

100%

🙏 帮我们做得更好

我们是刚上线的编程教程站,几个人的小团队,精力有限。页面虽经检查,难免还有疏漏——链接失效、排版错乱、内容有误、语言生硬……

如果您发现了,麻烦告诉我们,我们会在收到反馈后第一时间进行修复,再次感谢您的光临 🙏