Laravel: Laravel事件系统与广播
最后更新:2026-08-26
事件系统是 Laravel 的"通信网络"——发送方广播消息,接收方各自处理,发送方不用知道谁在听。
1. 你将学到
- 事件与监听器:EventServiceProvider 注册与自动发现
- 事件调度:event() vs Event::dispatch()
- 广播机制:Redis Pub/Sub + Laravel Echo + Pusher
- Channel 类型:Public/Private/Presence Channel
- 前端接收:Laravel Echo + WebSocket 实时通知
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) 广播架构
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 如何测试事件监听器?
A 用
Event::fake() 模拟事件调度,断言事件被调度:Event::assertDispatched(OrderPlaced::class)。监听器单独写单元测试。📖 小节
- 事件解耦发送方和接收方,新增功能只需加监听器
- ShouldQueue 监听器异步执行,不阻塞请求
- ShouldBroadcast 接口让事件通过 WebSocket 广播
- Private Channel 需授权,Presence Channel 还能看在线列表
- Laravel Echo 前端库统一处理 WebSocket 连接和事件监听
- broadcastWith() 控制广播数据量,只传必要字段
📝 作业
-
基础题(⭐):创建 OrderPlaced 事件和 SendOrderNotification 监听器,在订单创建时触发,用
Event::fake()编写测试验证事件被调度。 -
进阶题(⭐⭐):实现 OrderPlaced 事件的 ShouldBroadcast 广播,配置 Redis 广播驱动,用 Laravel Echo 在前端监听 private-tenant.{id} 频道,实时显示新订单通知。
-
挑战题(⭐⭐⭐):实现 Presence Channel 的多人协作仪表盘——多个用户同时查看同一商店数据时,显示在线用户列表,某用户修改数据时其他用户实时看到变化(订单状态变更广播)。