Broadcasting
Com implementar comunicació en temps real amb Laravel Broadcasting: WebSockets, canals, events, Laravel Echo, Reverb i integració amb el frontend.
Què és el Broadcasting?#
La majoria d'aplicacions web funcionen amb un model de petició-resposta: el client fa una petició, el servidor la processa i retorna una resposta. Però hi ha situacions on el servidor necessita enviar informació al client sense que el client la demani. Un missatge de xat nou, una notificació en temps real, una actualització d'estoc en directe, el progrés d'una importació llarga. Totes aquestes situacions requereixen comunicació del servidor cap al client, i això és exactament el que fa el Broadcasting de Laravel.
El Broadcasting permet que el teu servidor Laravel emeti events que els clients (navegadors) reben instantàniament a través de WebSockets. En lloc que el client hagi de consultar repetidament el servidor preguntant "hi ha novetats?" (polling), el servidor envia les dades directament al client en el moment que estan disponibles. Això resulta en aplicacions més ràpides, més eficients i amb una experiència d'usuari molt més fluida.
Per què importa el temps real?#
Les aplicacions modernes han establert unes expectatives d'immediatesa que el model petició-resposta tradicional no pot satisfer. Quan un usuari envia un missatge en un xat, l'altre participant espera veure'l instantàniament, no després de recarregar la pàgina. Quan una comanda canvia d'estat, el client vol veure l'actualització al moment. Quan diversos usuaris col·laboren en un document, cadascú necessita veure els canvis dels altres en temps real.
Sense Broadcasting, les alternatives són limitades i ineficients. El polling (fer peticions AJAX cada pocs segons) consumeix recursos del servidor innecessàriament i sempre té un retard inherent. El long polling millora una mica però segueix sent un hack. Els WebSockets, en canvi, estableixen una connexió persistent i bidireccional entre el client i el servidor, permetent que les dades fluixin instantàniament en ambdues direccions.
Laravel Broadcasting integra el sistema d'events de Laravel amb WebSockets de manera elegant. Quan dispares un event al servidor, Laravel el serialitza i l'envia a un servidor WebSocket, que el retransmet a tots els clients subscrits al canal corresponent. Tot aquest flux està abstret darrere d'una API senzilla que fa que implementar funcionalitat en temps real sigui sorprenentment fàcil.
Com funciona el Broadcasting#
El flux del Broadcasting a Laravel segueix un camí clar des que un event es dispara al servidor fins que arriba al navegador del client:
- El servidor dispara un event que implementa
ShouldBroadcast - Laravel serialitza l'event i l'envia al driver de broadcast configurat (Reverb, Pusher, Ably)
- El servidor WebSocket rep l'event i el retransmet als clients subscrits al canal
- Laravel Echo (al frontend) rep l'event i executa el callback associat
Aquesta arquitectura desacobla completament el backend del frontend. El servidor no necessita saber quants clients hi ha connectats ni gestionar connexions WebSocket directament. El servidor WebSocket (Reverb o Pusher) s'encarrega de tot el transport en temps real. Laravel només ha de dir "ha passat això en aquest canal" i el servidor WebSocket s'ocupa de la distribució.
Drivers disponibles#
Laravel suporta diversos drivers per al Broadcasting, cadascun amb característiques i casos d'ús diferents:
Reverb és el servidor WebSocket oficial de Laravel, introduït a Laravel 11. És la opció recomanada per defecte perquè és completament autogestionat (self-hosted), gratuït, sense límits de connexions ni de missatges, i està optimitzat específicament per a Laravel. Si comences un projecte nou, Reverb hauria de ser la teva primera elecció.
Pusher és un servei extern que gestiona tot el transport WebSocket per tu. No necessites instal·lar ni mantenir cap servidor WebSocket: Pusher ho fa tot al núvol. L'avantatge és la simplicitat; el desavantatge és que és un servei de pagament amb límits de connexions i missatges segons el pla.
Ably és un servei similar a Pusher però amb funcionalitats addicionals com la garantia de lliurament de missatges i la presència global. És ideal per a aplicacions que necessiten una fiabilitat extrema o una distribució geogràfica àmplia.
Redis amb Socket.IO era l'opció habitual abans de Reverb. Utilitza Redis com a intermediari i un servidor Node.js amb Socket.IO per gestionar les connexions WebSocket. Encara funciona, però Reverb és una alternativa nativa molt més senzilla de configurar i mantenir.
Instal·lació i configuració#
Laravel 11 simplifica enormement la instal·lació del Broadcasting. Una sola comanda configura tot el necessari:
php artisan install:broadcastingAquesta comanda fa diverses coses: publica el fitxer de configuració config/broadcasting.php, crea el fitxer routes/channels.php per a les autoritzacions de canals, instal·la Laravel Reverb com a servidor WebSocket i configura Laravel Echo al frontend. Si estàs usant un projecte Laravel nou, aquesta és la manera més ràpida de començar.
La variable d'entorn principal és BROADCAST_CONNECTION, que determina quin driver s'utilitza:
# Reverb (recomanat)
BROADCAST_CONNECTION=reverb
# Pusher
BROADCAST_CONNECTION=pusher
# Redis
BROADCAST_CONNECTION=redis
# Log (per a desenvolupament i debugging)
BROADCAST_CONNECTION=logEl driver log és especialment útil durant el desenvolupament: en lloc d'enviar events per WebSocket, els escriu al fitxer de log de Laravel. Això permet verificar que els events es disparen correctament sense necessitat de tenir un servidor WebSocket funcionant.
La configuració completa es defineix al fitxer config/broadcasting.php:
// config/broadcasting.php
return [
'default' => env('BROADCAST_CONNECTION', 'reverb'),
'connections' => [
'reverb' => [
'driver' => 'reverb',
'key' => env('REVERB_APP_KEY'),
'secret' => env('REVERB_APP_SECRET'),
'app_id' => env('REVERB_APP_ID'),
'options' => [
'host' => env('REVERB_HOST'),
'port' => env('REVERB_PORT', 443),
'scheme' => env('REVERB_SCHEME', 'https'),
'useTLS' => env('REVERB_SCHEME', 'https') === 'https',
],
],
'pusher' => [
'driver' => 'pusher',
'key' => env('PUSHER_APP_KEY'),
'secret' => env('PUSHER_APP_SECRET'),
'app_id' => env('PUSHER_APP_ID'),
'options' => [
'cluster' => env('PUSHER_APP_CLUSTER'),
'useTLS' => true,
],
],
],
];Laravel Reverb#
Laravel Reverb és el servidor WebSocket oficial de primera part per a Laravel. A diferència de Pusher o Ably, Reverb s'executa a la teva pròpia infraestructura, cosa que significa que no tens límits de connexions, no depens de serveis externs i les dades mai surten dels teus servidors. Per a la majoria d'aplicacions Laravel, Reverb és la millor opció.
Instal·lació#
Si no has executat php artisan install:broadcasting, pots instal·lar Reverb manualment:
composer require laravel/reverbDesprés, publica la configuració i executa les migracions:
php artisan reverb:installAixò crea el fitxer de configuració config/reverb.php i afegeix les variables d'entorn necessàries al teu .env:
REVERB_APP_ID=my-app-id
REVERB_APP_KEY=my-app-key
REVERB_APP_SECRET=my-app-secret
REVERB_HOST="localhost"
REVERB_PORT=8080
REVERB_SCHEME=http
# Variables per a Echo (frontend)
VITE_REVERB_APP_KEY="${REVERB_APP_KEY}"
VITE_REVERB_HOST="${REVERB_HOST}"
VITE_REVERB_PORT="${REVERB_PORT}"
VITE_REVERB_SCHEME="${REVERB_SCHEME}"Executar Reverb#
Per iniciar el servidor WebSocket en desenvolupament:
php artisan reverb:startAixò arrenca un servidor WebSocket que escolta al port configurat (per defecte 8080). El servidor és lleuger i eficient, capaç de gestionar milers de connexions simultànies en un sol procés.
Per a desenvolupament, pots fer que Reverb mostri informació de depuració:
php artisan reverb:start --debugConfiguració de Reverb#
El fitxer config/reverb.php permet configurar els detalls del servidor:
// config/reverb.php
return [
'default' => env('REVERB_SERVER', 'reverb'),
'servers' => [
'reverb' => [
'host' => env('REVERB_SERVER_HOST', '0.0.0.0'),
'port' => env('REVERB_SERVER_PORT', 8080),
'hostname' => env('REVERB_HOST'),
'options' => [
'tls' => [],
],
'max_request_size' => env('REVERB_MAX_REQUEST_SIZE', 10_000),
'scaling' => [
'enabled' => env('REVERB_SCALING_ENABLED', false),
'channel' => env('REVERB_SCALING_CHANNEL', 'reverb'),
],
'pulse_ingest_interval' => env('REVERB_PULSE_INGEST_INTERVAL', 15),
],
],
'apps' => [
'provider' => 'config',
'apps' => [
[
'key' => env('REVERB_APP_KEY'),
'secret' => env('REVERB_APP_SECRET'),
'app_id' => env('REVERB_APP_ID'),
'options' => [
'host' => env('REVERB_HOST'),
'port' => env('REVERB_PORT', 443),
'scheme' => env('REVERB_SCHEME', 'https'),
'useTLS' => env('REVERB_SCHEME', 'https') === 'https',
],
'allowed_origins' => ['*'],
'ping_interval' => env('REVERB_APP_PING_INTERVAL', 60),
'activity_timeout' => env('REVERB_APP_ACTIVITY_TIMEOUT', 30),
'max_message_size' => env('REVERB_APP_MAX_MESSAGE_SIZE', 10_000),
],
],
],
];Avantatges de Reverb sobre Pusher#
Per a la majoria d'aplicacions, Reverb és clarament superior a Pusher per diverses raons. La més evident és el cost: Reverb és gratuït i sense límits. Pusher té un pla gratuït limitat a 200.000 missatges diaris i 100 connexions simultànies. Per a una aplicació amb tràfic moderat, aquests límits es superen ràpidament.
A més, amb Reverb les dades mai surten dels teus servidors. Amb Pusher, cada missatge passa pel seu núvol, cosa que pot ser un problema per a aplicacions amb requisits de privacitat o compliment normatiu. Reverb també ofereix una integració perfecta amb Laravel Pulse per a monitorització en temps real del servidor WebSocket.
El principal avantatge de Pusher sobre Reverb és que no necessites gestionar infraestructura addicional. Si no vols preocupar-te per configurar Supervisor, Nginx i mantenir un servidor WebSocket en producció, Pusher elimina tota aquesta complexitat.
Reverb en producció#
En producció, Reverb necessita funcionar com un procés de llarga durada gestionat per Supervisor, igual que els workers de cues:
# /etc/supervisor/conf.d/reverb.conf
[program:reverb]
process_name=%(program_name)s
command=php /var/www/app/artisan reverb:start --host=0.0.0.0 --port=8080
autostart=true
autorestart=true
user=www-data
redirect_stderr=true
stdout_logfile=/var/www/app/storage/logs/reverb.log
stopwaitsecs=3600Per a un entorn de producció complet, normalment configuraràs Nginx com a proxy invers per gestionar TLS i redirigir les connexions WebSocket al port de Reverb:
# Configuració Nginx per a Reverb amb WebSocket
server {
listen 443 ssl;
server_name ws.example.com;
ssl_certificate /path/to/cert.pem;
ssl_certificate_key /path/to/key.pem;
location / {
proxy_http_version 1.1;
proxy_set_header Host $http_host;
proxy_set_header Scheme $scheme;
proxy_set_header SERVER_PORT $server_port;
proxy_set_header REMOTE_ADDR $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "Upgrade";
proxy_pass http://0.0.0.0:8080;
}
}La línia clau és proxy_set_header Upgrade $http_upgrade juntament amb Connection "Upgrade". Aquestes capçaleres són necessàries perquè Nginx permeti l'actualització de la connexió HTTP a WebSocket. Sense elles, la connexió WebSocket falla amb un error.
Crear events de broadcast#
Un event que es pot emetre per Broadcasting és un event normal de Laravel que implementa la interfície ShouldBroadcast. Aquesta interfície obliga a definir el mètode broadcastOn(), que indica a quins canals s'emet l'event. La resta de mètodes són opcionals i permeten personalitzar el nom de l'event, les dades que s'envien i les condicions d'emissió.
L'event bàsic#
php artisan make:event OrderStatusUpdatednamespace App\Events;
use App\Models\Order;
use Illuminate\Broadcasting\Channel;
use Illuminate\Broadcasting\InteractsWithSockets;
use Illuminate\Broadcasting\PrivateChannel;
use Illuminate\Contracts\Broadcasting\ShouldBroadcast;
use Illuminate\Foundation\Events\Dispatchable;
use Illuminate\Queue\SerializesModels;
class OrderStatusUpdated implements ShouldBroadcast
{
use Dispatchable, InteractsWithSockets, SerializesModels;
public function __construct(
public Order $order
) {}
public function broadcastOn(): array
{
return [
new PrivateChannel("orders.{$this->order->user_id}"),
];
}
}Quan es dispara aquest event amb OrderStatusUpdated::dispatch($order), Laravel el serialitza i l'envia al servidor WebSocket. Tots els clients subscrits al canal orders.{userId} rebran l'event amb les dades del model Order com a payload.
Per defecte, totes les propietats públiques de l'event s'inclouen al payload que rep el client. En aquest cas, el client rebrà l'objecte order complet serialitzat com a JSON.
ShouldBroadcast vs ShouldBroadcastNow#
La interfície ShouldBroadcast encua l'emissió de l'event per ser processada en segon pla per un worker de cues. Això és la opció correcta en la majoria de casos perquè no bloqueja la petició HTTP. Però si necessites que l'event s'emeti immediatament, sense passar per la cua, utilitza ShouldBroadcastNow:
use Illuminate\Contracts\Broadcasting\ShouldBroadcastNow;
class UrgentAlert implements ShouldBroadcastNow
{
use Dispatchable, InteractsWithSockets, SerializesModels;
public function __construct(
public string $message,
public string $level = 'critical'
) {}
public function broadcastOn(): array
{
return [
new Channel('alerts'),
];
}
}ShouldBroadcastNow és útil per a alertes urgents o situacions on el retard d'una cua (encara que sigui de mil·lisegons) no és acceptable. Però recorda que bloquejarà la petició HTTP fins que l'event s'hagi enviat al servidor WebSocket.
Si la teva aplicació utilitza cues amb el driver sync (el driver per defecte en desenvolupament), ShouldBroadcast i ShouldBroadcastNow es comporten exactament igual: ambdós emeten immediatament. La diferència només es nota amb drivers reals com Redis o database.
Personalitzar el nom de l'event#
Per defecte, el nom de l'event que rep el client és el nom complet de la classe PHP (per exemple, App\Events\OrderStatusUpdated). Pots personalitzar-lo amb broadcastAs() per fer-lo més curt i amigable:
class OrderStatusUpdated implements ShouldBroadcast
{
use Dispatchable, InteractsWithSockets, SerializesModels;
public function __construct(
public Order $order
) {}
public function broadcastOn(): array
{
return [
new PrivateChannel("orders.{$this->order->user_id}"),
];
}
public function broadcastAs(): string
{
return 'order.updated';
}
}Quan uses broadcastAs(), has d'afegir un punt (.) al davant del nom de l'event quan l'escoltes amb Echo al frontend: .listen('.order.updated', callback). El punt indica a Echo que el nom és literal i no ha d'afegir el prefix del namespace.
Personalitzar el payload#
Per defecte, totes les propietats públiques de l'event s'envien com a payload. Si vols controlar exactament quines dades rep el client, implementa broadcastWith():
class OrderStatusUpdated implements ShouldBroadcast
{
use Dispatchable, InteractsWithSockets, SerializesModels;
public function __construct(
public Order $order
) {}
public function broadcastOn(): array
{
return [
new PrivateChannel("orders.{$this->order->user_id}"),
];
}
public function broadcastWith(): array
{
return [
'id' => $this->order->id,
'status' => $this->order->status,
'total' => $this->order->total,
'updated_at' => $this->order->updated_at->toISOString(),
];
}
}broadcastWith() és important per dues raons. Primera, et permet enviar només les dades necessàries en lloc de tot el model, reduint la mida del payload. Segona, et permet transformar les dades al format que espera el frontend, per exemple convertint dates a format ISO o calculant valors derivats.
Emissió condicional#
De vegades, un event només s'ha d'emetre en certes condicions. El mètode broadcastWhen() permet decidir si l'event s'emet basant-se en la lògica que necessitis:
class OrderStatusUpdated implements ShouldBroadcast
{
use Dispatchable, InteractsWithSockets, SerializesModels;
public function __construct(
public Order $order
) {}
public function broadcastOn(): array
{
return [
new PrivateChannel("orders.{$this->order->user_id}"),
];
}
public function broadcastWhen(): bool
{
// Només emetre si l'estat ha canviat realment
return $this->order->wasChanged('status');
}
}Si broadcastWhen() retorna false, l'event es dispara normalment (els listeners s'executen) però no s'emet per WebSocket. Això és útil quan vols que la lògica del servidor s'executi sempre però la notificació al client només en certs casos.
Tipus de canals#
Laravel Broadcasting defineix tres tipus de canals, cadascun amb un nivell diferent d'accés i funcionalitat. Triar el canal correcte és essencial per a la seguretat i la funcionalitat de la teva aplicació.
Canals públics#
Un canal públic és accessible per qualsevol persona que conegui el nom del canal. No requereix autenticació ni autorització. Qualsevol client que es connecti al canal rebrà tots els events que s'hi emetin:
use Illuminate\Broadcasting\Channel;
public function broadcastOn(): array
{
return [
new Channel('news'),
];
}Els canals públics són adequats per a informació que no és sensible: actualitzacions generals, notícies, resultats esportius, indicadors de presència global. No els utilitzis per a dades privades d'un usuari, com comandes, missatges o informació financera.
Canals privats#
Un canal privat requereix que el client s'autentiqui i estigui autoritzat per accedir-hi. Quan un client intenta subscriure's a un canal privat, Laravel Echo fa una petició al servidor per verificar que l'usuari té permís. Si l'autorització falla, la subscripció es rebutja:
use Illuminate\Broadcasting\PrivateChannel;
public function broadcastOn(): array
{
return [
new PrivateChannel("orders.{$this->order->user_id}"),
];
}L'autorització es defineix al fitxer routes/channels.php. Cada canal privat necessita una regla d'autorització que rep l'usuari autenticat i els paràmetres del canal:
// routes/channels.php
use App\Models\User;
use App\Models\Order;
// L'usuari només pot escoltar el seu propi canal de comandes
Broadcast::channel('orders.{userId}', function (User $user, int $userId) {
return $user->id === $userId;
});
// Autorització basada en una relació del model
Broadcast::channel('projects.{projectId}', function (User $user, int $projectId) {
return $user->projects()->where('id', $projectId)->exists();
});La closure d'autorització rep l'usuari autenticat com a primer paràmetre i els segments del nom del canal com a paràmetres addicionals. Ha de retornar true per autoritzar l'accés o false per denegar-lo. Si el canal conté un wildcard (com {userId}), el valor real es passa com a argument.
Canals de presència#
Un canal de presència és com un canal privat amb una funcionalitat extra: permet saber quins usuaris estan connectats al canal en cada moment. Això és ideal per a funcionalitats com "qui està en línia", "qui està escrivint" o "qui està veient aquest document":
use Illuminate\Broadcasting\PresenceChannel;
public function broadcastOn(): array
{
return [
new PresenceChannel("chat.{$this->roomId}"),
];
}L'autorització d'un canal de presència és similar a la d'un canal privat, però en lloc de retornar true, ha de retornar un array amb la informació de l'usuari que es compartirà amb els altres membres del canal:
// routes/channels.php
Broadcast::channel('chat.{roomId}', function (User $user, int $roomId) {
if ($user->canAccessRoom($roomId)) {
return [
'id' => $user->id,
'name' => $user->name,
'avatar' => $user->avatar_url,
];
}
return false;
});Quan l'autorització retorna un array, l'usuari queda autoritzat i les dades de l'array es distribueixen a tots els membres del canal. Quan retorna false o null, l'accés es denega. Aquesta informació és la que els altres clients veuen quan consulten qui està al canal.
Laravel Echo (frontend)#
Laravel Echo és la llibreria JavaScript que es connecta al servidor WebSocket i facilita l'escolta d'events al navegador. Echo abstreu les complexitats del protocol WebSocket i proporciona una API fluida per subscriure's a canals, escoltar events i gestionar la presència.
Instal·lació#
Si has executat php artisan install:broadcasting, Echo ja està instal·lat i configurat. Si necessites instal·lar-lo manualment:
npm install --save-dev laravel-echo pusher-jsEl paquet pusher-js es necessita fins i tot amb Reverb, perquè Reverb implementa el protocol de Pusher. Echo utilitza pusher-js com a transport WebSocket independentment del servidor que utilitzis.
Configuració#
La configuració d'Echo normalment es fa al fitxer resources/js/echo.js o directament al bootstrap.js:
// resources/js/echo.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 ?? 80,
wssPort: import.meta.env.VITE_REVERB_PORT ?? 443,
forceTLS: (import.meta.env.VITE_REVERB_SCHEME ?? 'https') === 'https',
enabledTransports: ['ws', 'wss'],
});Si fas servir Pusher en lloc de Reverb:
window.Echo = new Echo({
broadcaster: 'pusher',
key: import.meta.env.VITE_PUSHER_APP_KEY,
cluster: import.meta.env.VITE_PUSHER_APP_CLUSTER,
forceTLS: true,
});Escoltar canals públics#
Per subscriure't a un canal públic i escoltar events:
// Escoltar l'event amb el nom complet de la classe
Echo.channel('news')
.listen('NewsPublished', (event) => {
console.log('Notícia nova:', event.title);
});
// Si l'event utilitza broadcastAs(), afegeix un punt al davant
Echo.channel('news')
.listen('.news.published', (event) => {
console.log('Notícia nova:', event.title);
});El punt inicial (.) és important quan l'event ha personalitzat el nom amb broadcastAs(). Sense el punt, Echo afegeix automàticament un prefix de namespace (com App\Events\). Amb el punt, Echo utilitza el nom exacte que has definit.
Escoltar canals privats#
Per als canals privats, utilitza Echo.private() en lloc de Echo.channel(). Echo farà automàticament una petició d'autorització al servidor abans de permetre la subscripció:
Echo.private(`orders.${userId}`)
.listen('OrderStatusUpdated', (event) => {
console.log('Comanda actualitzada:', event.order);
updateOrderStatus(event.order.id, event.order.status);
})
.listen('OrderShipped', (event) => {
showNotification(`La teva comanda ${event.order.id} s'ha enviat!`);
});Pots encadenar múltiples crides .listen() per escoltar diversos events al mateix canal. Cada crida registra un callback per a un event diferent.
Canals de presència#
Els canals de presència ofereixen funcionalitats addicionals per gestionar la llista d'usuaris connectats:
Echo.join(`chat.${roomId}`)
.here((users) => {
// Es crida quan et subscrius: rep la llista completa d'usuaris
console.log('Usuaris actuals:', users);
updateUserList(users);
})
.joining((user) => {
// Un nou usuari s'ha unit al canal
console.log(`${user.name} s'ha unit`);
addUserToList(user);
})
.leaving((user) => {
// Un usuari ha sortit del canal
console.log(`${user.name} ha marxat`);
removeUserFromList(user);
})
.listen('MessageSent', (event) => {
// Events normals funcionen igual que en canals privats
addMessage(event.message);
});El mètode .here() es crida una sola vegada quan la subscripció té èxit i rep un array amb tots els usuaris que ja estan al canal. Els mètodes .joining() i .leaving() es criden cada vegada que un usuari entra o surt del canal. Les dades de cada usuari són les que la closure d'autorització retorna al routes/channels.php.
Events de client (Whisper)#
Els events de client permeten que un navegador enviï events directament a altres navegadors subscrits al mateix canal, sense passar pel servidor Laravel. Això és útil per a funcionalitats com "l'usuari està escrivint" on la latència mínima és important i no cal processar res al servidor:
// Enviar un event de client
Echo.private(`chat.${roomId}`)
.whisper('typing', {
user: currentUser.name,
});
// Escoltar un event de client
Echo.private(`chat.${roomId}`)
.listenForWhisper('typing', (event) => {
showTypingIndicator(event.user);
});Els whispers només funcionen en canals privats i de presència (no en canals públics, per seguretat). L'event es transmet directament del navegador emissor als altres navegadors a través del servidor WebSocket, sense passar per Laravel.
Els events de client (whisper) no passen pel servidor Laravel, cosa que significa que no es poden validar ni registrar al servidor. No els utilitzis per a operacions que requereixin seguretat o auditoria. Utilitza'ls només per a funcionalitats efímeres com indicadors d'escriptura o posicions de cursor.
Canal de notificacions#
Laravel Echo té suport especial per escoltar les notificacions de Laravel. Cada usuari autenticat té un canal privat de notificacions (App.Models.User.{id}) on pot rebre les notificacions broadcast:
Echo.private(`App.Models.User.${userId}`)
.notification((notification) => {
console.log('Nova notificació:', notification.type);
if (notification.type === 'App\\Notifications\\OrderShipped') {
showToast(`Comanda ${notification.order_id} enviada!`);
}
});El canal de notificacions és diferent dels canals normals: no escoltes events específics sinó que reps totes les notificacions broadcast que s'envien a l'usuari. Això simplifica la gestió de notificacions al frontend.
Broadcasting de notificacions#
Per enviar notificacions a través del canal broadcast, la classe de notificació ha d'incloure 'broadcast' a l'array de canals retornat per via() i implementar el mètode toBroadcast():
namespace App\Notifications;
use App\Models\Order;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Notifications\Messages\BroadcastMessage;
use Illuminate\Notifications\Notification;
class OrderShipped extends Notification implements ShouldQueue
{
use Queueable;
public function __construct(
public Order $order
) {}
public function via(object $notifiable): array
{
return ['mail', 'database', 'broadcast'];
}
public function toBroadcast(object $notifiable): BroadcastMessage
{
return new BroadcastMessage([
'order_id' => $this->order->id,
'status' => 'shipped',
'tracking_url' => $this->order->tracking_url,
'message' => "La teva comanda #{$this->order->id} s'ha enviat!",
]);
}
}El BroadcastMessage es serialitza com a JSON i s'envia al canal privat de l'usuari. Al frontend, l'escoltes amb .notification() com s'ha vist a la secció anterior. La notificació pot ser simultàniament per correu, base de dades i broadcast, cadascú amb el seu format propi.
Model broadcasting#
Laravel 11 introdueix el trait BroadcastsEvents que permet que un model Eloquent emeti events automàticament quan es crea, actualitza o elimina. Això és una manera molt convenient d'implementar actualitzacions en temps real sense crear classes d'events manualment:
namespace App\Models;
use Illuminate\Broadcasting\PrivateChannel;
use Illuminate\Database\Eloquent\BroadcastsEvents;
use Illuminate\Database\Eloquent\Model;
class Comment extends Model
{
use BroadcastsEvents;
protected $fillable = ['body', 'user_id', 'article_id'];
public function broadcastOn(string $event): array
{
return match ($event) {
'created' => [new PrivateChannel("articles.{$this->article_id}")],
'updated' => [new PrivateChannel("articles.{$this->article_id}")],
'deleted' => [new PrivateChannel("articles.{$this->article_id}")],
default => [],
};
}
}Amb el trait BroadcastsEvents, cada vegada que es crea, actualitza o elimina un comentari, Laravel emet automàticament un event al canal especificat. El nom de l'event al frontend segueix el patró .CommentCreated, .CommentUpdated o .CommentDeleted:
Echo.private(`articles.${articleId}`)
.listen('.CommentCreated', (event) => {
addComment(event.model);
})
.listen('.CommentUpdated', (event) => {
updateComment(event.model);
})
.listen('.CommentDeleted', (event) => {
removeComment(event.model.id);
});El model broadcasting és ideal per a actualitzacions CRUD en temps real. Si necessites més control sobre quines dades s'envien o en quines condicions, crea un event manual amb ShouldBroadcast.
Exemple pràctic: xat en temps real#
Construir un xat en temps real il·lustra perfectament com totes les peces del Broadcasting encaixen. Necessitem un event per als missatges nous, autorització del canal, un controlador per enviar missatges i el codi Echo al frontend.
L'event#
namespace App\Events;
use App\Models\Message;
use App\Models\User;
use Illuminate\Broadcasting\InteractsWithSockets;
use Illuminate\Broadcasting\PresenceChannel;
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 User $user
) {}
public function broadcastOn(): array
{
return [
new PresenceChannel("chat.{$this->message->room_id}"),
];
}
public function broadcastAs(): string
{
return 'message.sent';
}
public function broadcastWith(): array
{
return [
'id' => $this->message->id,
'body' => $this->message->body,
'user' => [
'id' => $this->user->id,
'name' => $this->user->name,
'avatar' => $this->user->avatar_url,
],
'created_at' => $this->message->created_at->toISOString(),
];
}
}Utilitzem un PresenceChannel per poder mostrar qui està a la sala de xat. El payload personalitzat amb broadcastWith() envia només les dades necessàries per al frontend, en lloc de serialitzar tot el model amb les seves relacions.
Autorització del canal#
// routes/channels.php
use App\Models\User;
use App\Models\Room;
Broadcast::channel('chat.{roomId}', function (User $user, int $roomId) {
$room = Room::find($roomId);
if ($room && $room->members()->where('user_id', $user->id)->exists()) {
return [
'id' => $user->id,
'name' => $user->name,
'avatar' => $user->avatar_url,
];
}
return false;
});La closure verifica que l'usuari és membre de la sala de xat. Si ho és, retorna les dades que es compartiran amb els altres membres (visible a .here(), .joining() i .leaving() al frontend). Si no ho és, retorna false i la subscripció es rebutja.
El controlador#
namespace App\Http\Controllers;
use App\Events\MessageSent;
use App\Models\Message;
use App\Models\Room;
use Illuminate\Http\Request;
class ChatController extends Controller
{
public function index(Room $room)
{
$this->authorize('view', $room);
$messages = $room->messages()
->with('user:id,name,avatar_url')
->latest()
->take(50)
->get()
->reverse()
->values();
return view('chat.room', compact('room', 'messages'));
}
public function store(Request $request, Room $room)
{
$this->authorize('sendMessage', $room);
$validated = $request->validate([
'body' => 'required|string|max:1000',
]);
$message = $room->messages()->create([
'body' => $validated['body'],
'user_id' => $request->user()->id,
]);
// Emetre l'event per WebSocket
MessageSent::dispatch($message, $request->user());
return response()->json($message, 201);
}
}El controlador és senzill: valida l'entrada, crea el missatge a la base de dades i dispara l'event de broadcast. L'event s'encua automàticament gràcies a ShouldBroadcast, de manera que la resposta HTTP es retorna immediatament al client que ha enviat el missatge. Els altres clients el reben per WebSocket.
El frontend amb Echo#
// Configurar Echo per a la sala de xat
const chatChannel = Echo.join(`chat.${roomId}`);
// Llistar qui està a la sala
chatChannel.here((users) => {
onlineUsers.value = users;
})
.joining((user) => {
onlineUsers.value.push(user);
showSystemMessage(`${user.name} s'ha unit a la sala`);
})
.leaving((user) => {
onlineUsers.value = onlineUsers.value.filter(u => u.id !== user.id);
showSystemMessage(`${user.name} ha sortit de la sala`);
});
// Escoltar missatges nous
chatChannel.listen('.message.sent', (event) => {
messages.value.push({
id: event.id,
body: event.body,
user: event.user,
created_at: event.created_at,
});
scrollToBottom();
});
// Indicador "està escrivint"
let typingTimeout = null;
function onInput() {
chatChannel.whisper('typing', {
user: currentUser.name,
});
}
chatChannel.listenForWhisper('typing', (event) => {
typingUser.value = event.user;
clearTimeout(typingTimeout);
typingTimeout = setTimeout(() => {
typingUser.value = null;
}, 2000);
});
// Enviar un missatge
async function sendMessage(body) {
const response = await fetch(`/rooms/${roomId}/messages`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-CSRF-TOKEN': csrfToken,
},
body: JSON.stringify({ body }),
});
if (!response.ok) {
showError('Error enviant el missatge');
}
}Tot el xat funciona amb tres elements que es combinen harmoniosament. El canal de presència gestiona la llista d'usuaris en línia. L'event message.sent transmet els missatges nous. Els whispers gestionen l'indicador "està escrivint" sense carregar el servidor.
Fixa't que el client que envia el missatge no necessita tractar-lo de manera especial. Pot afegir el missatge a la llista localment quan fa el POST (per a una resposta immediata) o esperar a rebre'l per WebSocket com tots els altres clients. La primera opció és millor per a l'experiència d'usuari perquè el missatge apareix instantàniament.
Depuració#
Depurar problemes de Broadcasting pot ser frustrant perquè el flux passa per diversos sistemes (Laravel, cua, servidor WebSocket, frontend). Aquí tens algunes estratègies per diagnosticar problemes:
Canvia el driver a log temporalment. Amb BROADCAST_CONNECTION=log, els events s'escriuen al fitxer de log de Laravel en lloc d'enviar-se per WebSocket. Això et permet verificar que els events es disparen correctament amb les dades esperades.
Utilitza la consola del navegador. Activa la depuració de Pusher/Echo al frontend per veure els events en temps real:
// Activar logs de Pusher al navegador
window.Pusher.logToConsole = true;Amb aquesta opció activada, veuràs a la consola del navegador les connexions, subscripcions, autoritzacions i events que arriben. Si no veus res, el problema és al servidor; si veus errors d'autorització, el problema és al routes/channels.php.
Verifica que Reverb està funcionant. Si utilitzes Reverb, assegura't que el procés està actiu:
php artisan reverb:start --debugEl mode --debug mostra informació detallada sobre cada connexió, subscripció i event que passa pel servidor WebSocket.
Comprova les cues. Si l'event implementa ShouldBroadcast (no ShouldBroadcastNow), necessita un worker de cues actiu per ser processat. Verifica que el worker està funcionant:
php artisan queue:work --verboseSi el worker no processa l'event, l'event s'acumularà a la cua sense arribar mai al servidor WebSocket.
Verifica l'autorització dels canals. Si un canal privat no funciona, comprova que la resposta d'autorització sigui correcta. Pots fer una petició directa a l'endpoint d'autorització:
# Testejar l'autorització manualment
curl -X POST http://localhost:8000/broadcasting/auth \
-H "Content-Type: application/json" \
-d '{"channel_name": "private-orders.1"}' \
--cookie "laravel_session=..."L'ordre de depuració recomanat és: primer verifica que l'event es dispara (driver log), després verifica que el worker el processa (cua), després verifica que Reverb el rep (--debug), i finalment verifica que Echo el rep al navegador (logToConsole).
Desplegament en producció#
Un desplegament de Broadcasting en producció amb Reverb requereix tres components funcionant en paral·lel: l'aplicació Laravel (amb Nginx/Apache), un worker de cues (per processar els events broadcast) i el servidor Reverb (per gestionar les connexions WebSocket). Cadascun d'aquests components ha de ser gestionat per Supervisor per garantir que es reinicien automàticament si fallen.
La configuració de Supervisor per als tres components:
# /etc/supervisor/conf.d/laravel-broadcasting.conf
# Worker de cues per processar events broadcast
[program:laravel-queue-worker]
process_name=%(program_name)s_%(process_num)02d
command=php /var/www/app/artisan queue:work redis --sleep=3 --tries=3 --max-time=3600
autostart=true
autorestart=true
stopasgroup=true
killasgroup=true
user=www-data
numprocs=2
redirect_stderr=true
stdout_logfile=/var/www/app/storage/logs/worker.log
stopwaitsecs=3600
# Servidor WebSocket Reverb
[program:reverb]
process_name=%(program_name)s
command=php /var/www/app/artisan reverb:start --host=0.0.0.0 --port=8080
autostart=true
autorestart=true
user=www-data
redirect_stderr=true
stdout_logfile=/var/www/app/storage/logs/reverb.log
stopwaitsecs=3600Quan desplegueu codi nou, cal reiniciar tant els workers com Reverb per assegurar que carreguen el codi actualitzat. Afegeix aquestes comandes al teu script de desplegament:
# Script de desplegament
php artisan migrate --force
php artisan config:cache
php artisan route:cache
php artisan view:cache
php artisan queue:restart
php artisan reverb:restartPer a aplicacions amb molt de tràfic de WebSocket, configura Nginx com a proxy invers per a Reverb. Això permet gestionar TLS (HTTPS/WSS) a Nginx en lloc de a Reverb, centralitzant la gestió de certificats:
# /etc/nginx/sites-available/websocket.conf
server {
listen 443 ssl;
server_name ws.example.com;
ssl_certificate /etc/letsencrypt/live/ws.example.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/ws.example.com/privkey.pem;
location / {
proxy_http_version 1.1;
proxy_set_header Host $http_host;
proxy_set_header Scheme $scheme;
proxy_set_header SERVER_PORT $server_port;
proxy_set_header REMOTE_ADDR $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "Upgrade";
proxy_pass http://127.0.0.1:8080;
}
}Amb aquesta configuració, els clients es connecten a wss://ws.example.com (WebSocket segur) i Nginx s'encarrega de la terminació TLS i de redirigir el tràfic al port 8080 de Reverb. Recorda actualitzar les variables d'entorn per reflectir el domini i el port del proxy:
REVERB_HOST="ws.example.com"
REVERB_PORT=443
REVERB_SCHEME=https
VITE_REVERB_APP_KEY="${REVERB_APP_KEY}"
VITE_REVERB_HOST="ws.example.com"
VITE_REVERB_PORT=443
VITE_REVERB_SCHEME=https