Laravel: Laravel事件系统与广播

最后更新:2026-08-26

事件系统是 Laravel 的"通信网络"——发送方广播消息,接收方各自处理,发送方不用知道谁在听。

1. 你将学到


2. 一个产品经理的真实故事

(1) 痛点:订单状态变更要手动刷新才知道

Alice 在 ShopMetrics 后台管理订单——客户下单后她看不到,必须每 5 分钟手动刷新一次页面。Bob 更惨,他同时管理 3 个商店,刷新不过来,错过了 5 个紧急订单。Charlie 说:"2024 年了还在手动刷新?WebSocket 实时推送了解一下。"

(2) 事件广播的解法

Laravel 事件广播——订单创建时触发事件,服务器通过 WebSocket 推送到前端,Alice 的页面自动刷新,0 延迟。

PHP
// Order placed → event dispatched → WebSocket broadcast
event(new OrderPlaced($order));
// Alice's browser receives notification in real-time

(3) 收益

Alice 用实时通知后,新订单 0.5 秒内出现在仪表盘,再也不会漏单。


3. 事件与监听器

(1) 创建事件和监听器

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) 事件类

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) 监听器类

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) 注册事件监听器

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

▶ 示例:ShopMetrics 订单事件触发

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!');
}

输出:

TEXT 📖 仅展示
// 执行成功

4. 事件调度

(1) 调度方式

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) 同步 vs 异步监听器

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),
        ]);
    }
}
类型 接口 执行方式 适合场景
同步 请求内顺序执行 更新数据库
异步 ShouldQueue 推入队列异步执行 发邮件/Webhook
事务后 AfterCommit 事务提交后执行 依赖已持久化的数据

▶ 示例:ShopMetrics 事件与监听器注册

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
        ],
    ];
}

输出:

TEXT 📖 仅展示
// 执行成功

5. 广播机制

(1) 广播架构

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) 广播配置

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) 广播驱动对比

驱动 服务 自托管 性能 成本
Pusher Pusher Cloud 付费
Redis Redis + Laravel Echo Server 免费
Ably Ably Cloud 付费
Log 日志(开发用) 免费

▶ 示例:ShopMetrics 广播事件定义

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';
    }
}

输出:

TEXT 📖 仅展示
// 执行成功

6. Channel 类型与授权

(1) 三种 Channel

Channel 前缀 可见性 用途
Public channel- 所有人 公告、全站通知
Private private- 授权用户 租户/用户专属
Presence presence- 授权+在线列表 协作、聊天

(2) Channel 授权

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];
    }
});

▶ 示例:ShopMetrics Channel 授权

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;
});

输出:

TEXT 📖 仅展示
// 执行成功

7. 前端接收

(1) 安装 Laravel Echo

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

(2) 配置 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) 监听事件

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);
    });

▶ 示例:ShopMetrics 实时仪表盘

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>

输出:

TEXT 📖 仅展示
// 执行成功

8. 综合示例:ShopMetrics 实时通知系统

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;
    }
}

❓ 常见问题

Q 事件和直接调用有什么区别?
A 直接调用 A→B 紧耦合,A 必须知道 B 的存在;事件 A→Event→B 松耦合,A 不知道谁在监听。新增功能只需加监听器,不修改发送方代码。
Q ShouldQueue 监听器什么时候用?
A 耗时操作(发邮件、调 Webhook、生成报表)用异步监听器;即时操作(更新数据库、清缓存)用同步监听器。异步监听器不会阻塞请求。
Q Pusher 和 Redis 广播怎么选?
A 小规模/快速上线用 Pusher(托管服务),大规模/成本敏感用 Redis + Laravel Echo Server(自托管)。Pusher 免费额度有限,Redis 服务器成本固定。
Q 前端收不到广播事件怎么办?
A 检查清单:1) QUEUE_CONNECTION 不是 sync 2) queue:work 在运行 3) Echo 配置的 key/host 正确 4) Channel 授权返回 true 5) 广播事件实现了 ShouldBroadcast。
Q 广播事件数据太大怎么办?
A broadcastWith() 只传必要字段(ID+关键信息),前端收到后再 AJAX 获取完整数据。避免在广播数据中嵌套大量关联资源。
Q 如何测试事件监听器?
AEvent::fake() 模拟事件调度,断言事件被调度:Event::assertDispatched(OrderPlaced::class)。监听器单独写单元测试。

📖 小节


📝 作业

  1. 基础题(⭐):创建 OrderPlaced 事件和 SendOrderNotification 监听器,在订单创建时触发,用 Event::fake() 编写测试验证事件被调度。

  2. 进阶题(⭐⭐):实现 OrderPlaced 事件的 ShouldBroadcast 广播,配置 Redis 广播驱动,用 Laravel Echo 在前端监听 private-tenant.{id} 频道,实时显示新订单通知。

  3. 挑战题(⭐⭐⭐):实现 Presence Channel 的多人协作仪表盘——多个用户同时查看同一商店数据时,显示在线用户列表,某用户修改数据时其他用户实时看到变化(订单状态变更广播)。

Web-Tutorial.com

Web-Tutorial 技术团队

由多位开发者共同维护的编程教程平台。每篇教程由对应领域的开发者编写和审核,确保内容准确可靠。如发现任何问题,欢迎向我们反馈。

100%

🙏 帮我们做得更好

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

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