404 Not Found

404 Not Found


nginx

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


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.

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

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) Classe de Evento

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) Classes de Listener

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

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

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

    // Despachar evento — aciona todos os listeners + broadcast
    event(new OrderPlaced($order));

    return redirect()->route('orders.show', $order)
        ->with('success', 'Order placed!');
}

Saída:

TEXT
// Execução bem-sucedida

4. Despacho de Eventos

(1) Método de Despacho

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

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

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

TEXT
// Execução bem-sucedida

5. Mecanismo de Broadcast

(1) Arquitetura de Broadcast

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

BASH
# .env
BROADCAST_CONNECTION=redis
QUEUE_CONNECTION=redis

# Instalar dependências
composer require pusher/pusher-php-server
# Ou para Redis:
# predis/predis já instalado
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) 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

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

Saída:

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

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

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

Saída:

TEXT
// Execução bem-sucedida

7. Recepção no Front-End

(1) Instalar Laravel Echo

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

(2) Configurar 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) Ouvindo Eventos

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

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

TEXT
// Execução bem-sucedida

8. Exemplo Abrangente: Sistema de Notificação em Tempo Real do ShopMetrics

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

P Qual é a diferença entre eventos e chamadas diretas?
R Uma chamada direta (A→B) é fortemente acoplada; A precisa estar ciente da existência de B. Um evento (A→Evento→B) é fracamente acoplado; A não sabe quem está ouvindo. Novos recursos podem ser adicionados simplesmente adicionando listeners, sem modificar o código do remetente.
P Quando o listener ShouldQueue deve ser usado?
R Use listeners assíncronos para operações demoradas (envio de emails, acionamento de webhooks, geração de relatórios); use listeners síncronos para operações imediatas (atualização do banco de dados, limpeza de cache). Listeners assíncronos não bloqueiam requisições.
P Como escolher entre Pusher e Redis para notificações push?
R Use Pusher (serviço hospedado) para implantações de pequena escala ou lançamentos rápidos; use Redis + Laravel Echo Server (auto-hospedado) para implantações de grande escala ou quando o custo é uma preocupação. Pusher tem cotas gratuitas limitadas, enquanto os custos do servidor Redis são fixos.
P O que devo fazer se o front-end não estiver recebendo eventos de broadcast?
R Lista de verificação: 1) QUEUE_CONNECTION não está definido como "sync" 2) queue:work está em execução 3) A chave/host configurada no Echo está correta 4) A autorização do canal retorna true 5) O evento de broadcast implementa ShouldBroadcast.
P O que devo fazer se os dados do evento de broadcast forem muito grandes?
R Use 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.
P Como testar listeners de eventos?
R Use 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


📝 Exercícios

  1. Exercício Básico (⭐): Crie o evento OrderPlaced e o listener SendOrderNotification, que é acionado quando um pedido é criado. Use Event::fake() para escrever um teste verificando que o evento foi despachado.

  2. Exercício Avançado (⭐⭐): Implemente o broadcast ShouldBroadcast para o evento OrderPlaced, configure o driver de broadcast Redis, e use Laravel Echo no frontend para ouvir o canal private-tenant.{id} para exibir notificações em tempo real de novos pedidos.

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

Web-Tutorial.com

Equipe Técnica Web-Tutorial

Uma plataforma de tutoriais mantida por diversos desenvolvedores. Cada tutorial é escrito e revisado por profissionais da área correspondente. Trabalhamos para manter nosso conteúdo preciso e confiável — se encontrar algum problema, avise-nos.

100%