Sistema de Eventos e Broadcasts no Laravel
O sistema de eventos é a "rede de comunicação" do Laravel—o remetente transmite uma mensagem, e os receptores cada um a trata por conta própria; o remetente não precisa saber quem está ouvindo.
1. O Que Você Vai Aprender
- Eventos e Listeners: Registro no EventServiceProvider e Auto-Discovery
- Despacho de Eventos: event() vs Event::dispatch()
- Mecanismo de Broadcasting: Redis Pub/Sub + Laravel Echo + Pusher
- Tipo de Canal: Canal Público/Privado/Presence
- Recepção no front-end: Laravel Echo + WebSocket notificações em tempo real
2. A História Real de um Gerente de Produto
(1) Dor: Você precisa atualizar a página manualmente para ver mudanças no status do pedido
Alice gerencia pedidos no back-end do ShopMetrics—ela não consegue vê-los logo após os clientes fazerem os pedidos e precisa atualizar a página manualmente a cada 5 minutos. Bob está pior; ele gerencia três lojas ao mesmo tempo, não consegue acompanhar as atualizações, e perdeu cinco pedidos urgentes. Charlie disse: "É 2024 e ainda estamos atualizando manualmente? Vocês já ouviram falar de notificações em tempo real via WebSocket?"
(2) Soluções com Broadcast de Eventos
Broadcast de Eventos do Laravel—Um evento é disparado quando um pedido é criado; o servidor envia o evento para o front-end via WebSocket, e a página de Alice é atualizada automaticamente com latência zero.
// Pedido realizado → evento despachado → broadcast via WebSocket
event(new OrderPlaced($order));
// O navegador de Alice recebe a notificação em tempo real
(3) Resultado
Com as notificações em tempo real de Alice, novos pedidos aparecem no dashboard em 0,5 segundos, então você nunca mais perderá um pedido.
3. Eventos e Listeners
(1) Criando Eventos e 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) Classe de Evento
// 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) Classes de Listener
// app/Listeners/SendOrderNotification.php
class SendOrderNotification
{
public function handle(OrderPlaced $event): void
{
$order = $event->order;
// Enviar notificação por email ao proprietário da loja
$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) Registrar um listener de evento
// app/Providers/EventServiceProvider.php
protected $listen = [
OrderPlaced::class => [
SendOrderNotification::class,
UpdateShopRevenue::class,
SendOrderWebhook::class,
],
OrderStatusChanged::class => [
SendStatusChangeNotification::class,
],
];
(1) ▶ Exemplo: Gatilho de Evento de Pedido do ShopMetrics
// 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;
});
// Despachar evento — aciona todos os listeners + broadcast
event(new OrderPlaced($order));
return redirect()->route('orders.show', $order)
->with('success', 'Order placed!');
}
Saída:
// Execução bem-sucedida
4. Despacho de Eventos
(1) Método de Despacho
// Método 1: helper event() (recomendado)
event(new OrderPlaced($order));
// Método 2: Facade Event
Event::dispatch(new OrderPlaced($order));
// Método 3: Despacho estático na classe de evento
OrderPlaced::dispatch($order);
(2) Listeners Síncronos vs. Assíncronos
// Listener síncrono — executa no ciclo da requisição
class UpdateShopRevenue implements ShouldHandleEventsAfterCommit
{
public function handle(OrderPlaced $event): void
{
$event->order->shop->increment('revenue', $event->order->total);
}
}
// Listener assíncrono — enviado para a fila
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),
]);
}
}
| Tipo | Interface | Método de Implementação | Cenários Adequados |
|---|---|---|---|
| Síncrono | Nenhum | Execução sequencial dentro da requisição | Atualizar banco de dados |
| Assíncrono | ShouldQueue | Enfileirar para execução assíncrona | Enviar email/Webhook |
| Pós-Transação | AfterCommit | Executado após a transação ser confirmada | Depende de dados persistidos |
(1) ▶ Exemplo: Eventos e Registro de Listeners do ShopMetrics
// app/Providers/EventServiceProvider.php
class EventServiceProvider extends ServiceProvider
{
protected $listen = [
// Eventos de pedido
OrderPlaced::class => [
UpdateShopRevenue::class, // síncrono — atualizar estatísticas
SendOrderNotification::class, // assíncrono — enviar email
SendOrderWebhook::class, // assíncrono — chamar webhook
],
OrderStatusChanged::class => [
SendStatusNotification::class, // assíncrono
UpdateAnalyticsCache::class, // síncrono — limpar cache
],
SubscriptionCreated::class => [
SendWelcomeEmail::class, // assíncrono
ProvisionTenantResources::class, // assíncrono
],
];
}
Saída:
// Execução bem-sucedida
5. Mecanismo de Broadcast
(1) Arquitetura de Broadcast
sequenceDiagram
participant S as Servidor
participant E as Evento
participant B as Broadcaster
participant WS as Servidor WebSocket
participant C as Cliente (Echo)
S->>E: event(new OrderPlaced($order))
E->>B: broadcastOn() → canais
B->>WS: Publicar no canal Redis
WS->>C: Push via WebSocket
C->>C: Echo recebe & atualiza UI
(2) Configuração de Broadcast
# .env
BROADCAST_CONNECTION=redis
QUEUE_CONNECTION=redis
# Instalar dependências
composer require pusher/pusher-php-server
# Ou para Redis:
# predis/predis já instalado
// 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) Comparação de Drivers de Broadcast
| Drivers | Serviços | Auto-hospedado | Desempenho | Custo |
|---|---|---|---|---|
| Pusher | Pusher Cloud | ❌ | Alto | Pago |
| Redis | Redis + Laravel Echo Server | ✅ | Alto | Gratuito |
| Ably | Ably Cloud | ❌ | Alto | Pago |
| Log | Log (para desenvolvimento) | ✅ | — | Gratuito |
(1) ▶ Exemplo: Definições de Eventos de Broadcast do ShopMetrics
// 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';
}
}
Saída:
// Execução bem-sucedida
6. Tipos de Canal e Autorização
(1) Três Tipos de Canais
| Canal | Prefixo | Visibilidade | Propósito |
|---|---|---|---|
| Público | channel- |
Todos | Anúncios, Avisos Gerais |
| Privado | private- |
Usuário Autorizado | Exclusivo de Tenant/Usuário |
| Presence | presence- |
Autorização + Lista Online | Colaboração, Chat |
(2) Autorização de Canal
// routes/channels.php
use Illuminate\Support\Facades\Broadcast;
// Canal privado — apenas membros do tenant podem ouvir
Broadcast::channel('tenant.{tenantId}', function ($user, $tenantId) {
return $user->tenant_id === (int) $tenantId;
});
// Canal privado — apenas proprietário/analista da loja
Broadcast::channel('shop.{shopId}', function ($user, $shopId) {
$shop = Shop::find($shopId);
return $shop && $user->tenant_id === $shop->tenant_id;
});
// Canal presence — quem está 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) ▶ Exemplo: Autorização de Canal do ShopMetrics
// 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;
});
Saída:
// Execução bem-sucedida
7. Recepção no Front-End
(1) Instalar Laravel Echo
npm install laravel-echo pusher-js
# Ou para Redis:
npm install laravel-echo-connector socket.io-client
(2) Configurar 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) Ouvindo Eventos
// Ouvir em canal privado — eventos específicos do tenant
window.Echo.private(`tenant.${tenantId}`)
.listen('.order.placed', (e) => {
showToast(`Novo pedido: ${e.order_number} — $${e.total}`);
updateOrdersList(e);
})
.listen('.order.status_changed', (e) => {
updateOrderStatus(e.order_id, e.new_status);
});
// Ouvir em canal presence — ver quem está online
window.Echo.join(`shop.dashboard.${shopId}`)
.here((users) => {
updateOnlineUsers(users);
})
.joining((user) => {
addOnlineUser(user);
})
.leaving((user) => {
removeOnlineUser(user);
});
(1) ▶ Exemplo: Dashboard em Tempo Real do ShopMetrics
<!-- resources/views/dashboard/index.blade.php -->
<script>
const tenantId = {{ auth()->user()->tenant_id }};
// Inicializar Echo
window.Echo.private(`tenant.${tenantId}`)
.listen('.order.placed', (event) => {
// Atualizar estatísticas
const stats = document.getElementById('stats');
const orderCount = stats.querySelector('.order-count');
orderCount.textContent = parseInt(orderCount.textContent) + 1;
// Adicionar aos pedidos recentes
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">Novo</span></td>
</tr>
`);
// Mostrar notificação
showNotification(`Novo pedido de ${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>
Saída:
// Execução bem-sucedida
8. Exemplo Abrangente: Sistema de Notificação em Tempo Real do ShopMetrics
// ============================================
// Abrangente: Notificações em Tempo Real do ShopMetrics
// Cobre: eventos, listeners, broadcast, canais, 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'; }
}
// Disparando na classe de serviço
class OrderService
{
public function place(array $data): Order
{
$order = DB::transaction(function () use ($data) {
$order = Order::create($data);
// ... criar itens, calcular 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;
}
}
❓ Perguntas Frequentes
broadcastWith() para enviar apenas os campos necessários (ID + informações-chave); uma vez que o front-end os recebe, pode usar AJAX para buscar os dados completos. Evite incorporar um grande número de recursos associados nos dados de broadcast.Event::fake() para simular o despacho de eventos, e afirme que o evento foi despachado: Event::assertDispatched(OrderPlaced::class). Escreva testes unitários separados para os listeners.📖 Resumo
- Desacopla remetentes e receptores de eventos; novos recursos podem ser adicionados simplesmente adicionando um listener
- O listener ShouldQueue executa de forma assíncrona e não bloqueia requisições
- A interface ShouldBroadcast permite que eventos sejam transmitidos via WebSocket
- Canais Privados exigem autorização; Canais Presence também exibem uma lista de usuários atualmente online
- A biblioteca front-end Laravel Echo gerencia centralmente conexões WebSocket e escuta de eventos
- broadcastWith() controla a quantidade de dados transmitidos, enviando apenas os campos necessários
📝 Exercícios
-
Exercício Básico (⭐): Crie o evento
OrderPlacede o listenerSendOrderNotification, que é acionado quando um pedido é criado. UseEvent::fake()para escrever um teste verificando que o evento foi despachado. -
Exercício Avançado (⭐⭐): Implemente o broadcast
ShouldBroadcastpara o eventoOrderPlaced, configure o driver de broadcast Redis, e use Laravel Echo no frontend para ouvir o canalprivate-tenant.{id}para exibir notificações em tempo real de novos pedidos. -
Desafio (⭐⭐⭐): Implemente um dashboard colaborativo multi-usuário usando o Canal Presence—quando múltiplos usuários visualizam os mesmos dados de loja simultaneamente, exiba uma lista de usuários online; quando um usuário modifica os dados, outros usuários veem as mudanças em tempo real (transmitindo mudanças de status do pedido).



