Broadcasting: eventos em tempo real com Laravel Echo

1. Fundamentos do Broadcasting no Laravel

O broadcasting no Laravel permite que você transmita eventos do servidor para o frontend em tempo real usando WebSockets. Diferente do modelo tradicional de requisição-resposta HTTP, onde o cliente precisa fazer polling constante para verificar novidades, o broadcasting estabelece uma conexão persistente bidirecional entre servidor e cliente.

A arquitetura se divide em três componentes principais:
- Servidor WebSocket: responsável por gerenciar as conexões em tempo real (Pusher, Laravel Reverb, Soketi)
- Laravel Echo: biblioteca JavaScript que facilita a escuta de eventos no frontend
- Driver de broadcasting: configuração no Laravel que define qual servidor WebSocket utilizar

Para instalar o suporte a broadcasting no Laravel 11, utilize:

composer require laravel/reverb

Em seguida, publique os assets de configuração:

php artisan reverb:install

No arquivo .env, configure o driver de broadcasting:

BROADCAST_DRIVER=reverb
REVERB_APP_ID=seu-app-id
REVERB_APP_KEY=seu-app-key
REVERB_APP_SECRET=seu-app-secret
REVERB_HOST=localhost
REVERB_PORT=8080

2. Criando e Disparando Eventos Broadcast

Para criar um evento que pode ser transmitido, utilize o comando Artisan e implemente a interface ShouldBroadcast:

php artisan make:event MessageSent

A classe de evento deve estender ShouldBroadcast e definir o canal onde será transmitido:

<?php

namespace App\Events;

use App\Models\Message;
use Illuminate\Broadcasting\Channel;
use Illuminate\Broadcasting\InteractsWithSockets;
use Illuminate\Contracts\Broadcasting\ShouldBroadcast;
use Illuminate\Foundation\Events\Dispatchable;
use Illuminate\Queue\SerializesModels;

class MessageSent implements ShouldBroadcast
{
    use Dispatchable, InteractsWithSockets, SerializesModels;

    public function __construct(
        public Message $message
    ) {}

    public function broadcastOn(): array
    {
        return [
            new Channel('chat.'.$this->message->chat_id),
        ];
    }

    public function broadcastAs(): string
    {
        return 'message.sent';
    }
}

Para disparar o evento, utilize o helper broadcast() ou a função event():

use App\Events\MessageSent;

// Disparo direto
broadcast(new MessageSent($message));

// Através de event() - útil para múltiplos listeners
event(new MessageSent($message));

// Com fila (recomendado para produção)
MessageSent::dispatch($message);

3. Autenticação de Canais Privados e Presença

Canais privados exigem que o usuário esteja autenticado para escutar os eventos. Configure as rotas de autorização em routes/channels.php:

use App\Models\User;
use App\Models\Chat;

Broadcast::channel('chat.{chatId}', function (User $user, int $chatId) {
    return $user->chats()->where('chat_id', $chatId)->exists();
});

Para canais de presença, que rastreiam usuários conectados:

Broadcast::channel('presence.chat.{chatId}', function (User $user, int $chatId) {
    if ($user->chats()->where('chat_id', $chatId)->exists()) {
        return ['id' => $user->id, 'name' => $user->name];
    }
});

A autorização pode utilizar Gates e Policies para lógicas mais complexas:

// app/Policies/ChatPolicy.php
public function join(User $user, Chat $chat): bool
{
    return $chat->participants()->where('user_id', $user->id)->exists();
}

// routes/channels.php
Broadcast::channel('chat.{chat}', function (User $user, Chat $chat) {
    return $user->can('join', $chat);
});

4. Laravel Echo no Frontend

No frontend, instale o Laravel Echo e o conector apropriado:

npm install --save-dev laravel-echo pusher-js

Configure o Echo no arquivo resources/js/bootstrap.js:

import Echo from 'laravel-echo';
import Pusher from 'pusher-js';

window.Pusher = Pusher;

window.Echo = new Echo({
    broadcaster: 'reverb',
    key: import.meta.env.VITE_REVERB_APP_KEY,
    wsHost: import.meta.env.VITE_REVERB_HOST,
    wsPort: import.meta.env.VITE_REVERB_PORT ?? 8080,
    wssPort: import.meta.env.VITE_REVERB_PORT ?? 443,
    forceTLS: (import.meta.env.VITE_REVERB_SCHEME ?? 'https') === 'https',
    enabledTransports: ['ws', 'wss'],
});

Para escutar eventos:

// Escuta de evento em canal público
Echo.channel('chat.1')
    .listen('.message.sent', (e) => {
        console.log('Nova mensagem:', e.message);
    });

// Escuta em canal privado
Echo.private('chat.1')
    .listen('.message.sent', (e) => {
        // Processar mensagem
    });

// Notificações
Echo.private(`App.Models.User.${userId}`)
    .notification((notification) => {
        console.log('Notificação recebida:', notification);
    });

// Whisper (mensagens temporárias como "digitando...")
Echo.private('chat.1')
    .whisper('typing', { user: user.name });

5. Broadcasting em Canais Específicos

Canais Públicos

Qualquer usuário pode escutar sem autenticação:

// Evento
public function broadcastOn(): array
{
    return [new Channel('public.news')];
}

// Frontend
Echo.channel('public.news')
    .listen('.news.updated', (e) => {
        // Atualizar feed de notícias
    });

Canais Privados

Exigem autenticação e são ideais para dados sensíveis:

// Evento
public function broadcastOn(): array
{
    return [new PrivateChannel('order.'.$this->order->user_id)];
}

// Frontend
Echo.private(`order.${userId}`)
    .listen('.status.changed', (e) => {
        // Atualizar status do pedido
    });

Canais de Presença

Permitem rastrear usuários conectados:

// Frontend
const presenceChannel = Echo.join(`presence.chat.${chatId}`);

presenceChannel
    .here((users) => {
        console.log('Usuários atualmente conectados:', users);
    })
    .joining((user) => {
        console.log(`${user.name} entrou no chat`);
    })
    .leaving((user) => {
        console.log(`${user.name} saiu do chat`);
    })
    .listen('.message.sent', (e) => {
        // Nova mensagem
    });

6. Integração com Laravel Reverb

O Laravel Reverb é um servidor WebSocket nativo escalável. Para configurá-lo:

php artisan reverb:start

Para produção, utilize um gerenciador de processos como Supervisor:

[program:reverb]
command=php artisan reverb:start
process_name=%(program_name)s_%(process_num)02d
numprocs=1
autostart=true
autorestart=true
user=forge

Exemplo prático de chat em tempo real:

// app/Events/ChatMessage.php
class ChatMessage implements ShouldBroadcast
{
    use Dispatchable, InteractsWithSockets, SerializesModels;

    public function __construct(
        public string $message,
        public string $userName,
        public int $chatId
    ) {}

    public function broadcastOn(): array
    {
        return [new PresenceChannel('chat.'.$this->chatId)];
    }

    public function broadcastAs(): string
    {
        return 'chat.message';
    }
}
// Controller
public function sendMessage(Request $request, Chat $chat)
{
    $message = $chat->messages()->create([
        'user_id' => auth()->id(),
        'content' => $request->content,
    ]);

    broadcast(new ChatMessage(
        message: $message->content,
        userName: auth()->user()->name,
        chatId: $chat->id
    ))->toOthers();

    return response()->json(['status' => 'sent']);
}

7. Boas Práticas e Depuração

Tratamento de Erros e Reconexão

Configure reconexão automática no Echo:

window.Echo = new Echo({
    // ... outras configurações
    reconnectionAttempts: 5,
    reconnectionDelay: 1000,
});

Testes com PHPUnit

Teste o broadcasting sem depender do servidor WebSocket:

use Illuminate\Support\Facades\Event;

public function test_message_broadcast()
{
    Event::fake();

    $message = Message::factory()->create();

    broadcast(new MessageSent($message));

    Event::assertDispatched(MessageSent::class, function ($event) use ($message) {
        return $event->message->id === $message->id;
    });
}

Monitoramento

No Reverb, ative logs para depuração:

// config/reverb.php
'logging' => [
    'channels' => ['stack'],
    'level' => env('REVERB_LOG_LEVEL', 'debug'),
],

Monitore conexões ativas via dashboard do Reverb ou Pusher. Utilize ferramentas como Laravel Telescope para inspecionar eventos broadcast em desenvolvimento.

Lembre-se de sempre usar filas para eventos broadcast em produção, evitando que o servidor WebSocket bloqueie a resposta HTTP. Configure a fila padrão no arquivo .env:

QUEUE_CONNECTION=database

Referências