Implementa WebSockets com Laravel Reverb para funcionalidades em tempo real. Use quando precisar adicionar WebSockets, broadcasting, eventos em tempo real, ou notificações push.
From laravel-toolkitnpx claudepluginhub aronpc/ai --plugin laravel-toolkitThis skill is limited to using the following tools:
Executes implementation plans in current session by dispatching fresh subagents per independent task, with two-stage reviews: spec compliance then code quality.
Processes code review feedback technically: verify suggestions against codebase, clarify unclear items, push back if questionable, implement after evaluation—not blind agreement.
Dispatches code-reviewer subagent to evaluate code changes via git SHAs after tasks, major features, or before merging, with focused context on implementation and requirements.
Implementa WebSockets com Laravel Reverb para comunicação em tempo real.
| Skill | Quando usar junto |
|---|---|
actions | Para broadcasting de events |
testing | Para testar WebSockets |
mcp | Para validar realtime no browser |
cicd | Para deploy com Reverb |
Use esta skill sempre que:
Backend: Laravel Reverb (WebSockets)
Frontend: React + Echo ou Browser WebSocket API
composer require laravel/reverb
php artisan reverb:install
npm install laravel-echo pusher-js
# .env
REVERB_APP_ID=local-app
REVERB_APP_KEY=local-key
REVERB_APP_SECRET=local-secret
REVERB_HOST=localhost
REVERB_PORT=8081
REVERB_SCHEME=http
VITE_REVERB_APP_KEY="${REVERB_APP_KEY}"
php artisan make:event OrderStatusChanged
<?php
declare(strict_types=1);
namespace App\Events;
use App\Models\Order;
use Illuminate\Broadcasting\Channel;
use Illuminate\Broadcasting\InteractsWithSockets;
use Illuminate\Broadcasting\PresenceChannel;
use Illuminate\Broadcasting\PrivateChannel;
use Illuminate\Contracts\Broadcasting\ShouldBroadcast;
use Illuminate\Foundation\Events\Dispatchable;
final class OrderStatusChanged implements ShouldBroadcast
{
use Dispatchable;
use InteractsWithSockets;
public function __construct(
public readonly Order $order,
public readonly string $oldStatus,
public readonly string $newStatus,
) {}
/**
* Canal público.
*/
public function broadcastOn(): Channel
{
return new Channel('orders');
}
/**
* Canal privado.
*/
public function broadcastOn(): PrivateChannel
{
return new PrivateChannel('orders.' . $this->order->id);
}
/**
* Canal presence.
*/
public function broadcastOn(): PresenceChannel
{
return new PresenceChannel('orders.' . $this->order->id);
}
}
// Em Actions ou Controllers
event(new OrderStatusChanged($order, $oldStatus, $newStatus));
// Ou com delay
event(new OrderStatusChanged($order, $oldStatus, $newStatus))->delay(now()->addSeconds(5));
// Event
public function broadcastOn(): Channel
{
return new Channel('public-updates');
}
// Autorização: Nenhuma
// Frontend: Echo.channel('public-updates')
// Event
public function broadcastOn(): PrivateChannel
{
return new PrivateChannel('orders.' . $this->order->id);
}
// Autorização: Required
// Frontend: Echo.private('orders.' . orderId)
// routes/channels.php
Broadcast::channel('orders.{order}', function ($user, Order $order) {
return $user->tenant_id === $order->tenant_id;
});
// Event
public function broadcastOn(): PresenceChannel
{
return new PresenceChannel('tracking.{orderId}');
}
// Autorização: Required
// Frontend: Echo.join('tracking.' + orderId)
// routes/channels.php
Broadcast::channel('tracking.{orderId}', function ($user, $orderId) {
$order = Order::find($orderId);
return $order && $user->tenant_id === $order->tenant_id;
});
<?php
// routes/channels.php
use App\Models\Order;
use App\Models\User;
use Illuminate\Support\Facades\Broadcast;
// Private channel - acesso por usuário
Broadcast::channel('orders.{order}', function (User $user, Order $order) {
return $user->tenant_id === $order->tenant_id;
});
// Private channel - acesso por tenant
Broadcast::channel('tenant.{tenantId}', function (User $user, int $tenantId) {
return $user->tenant_id === $tenantId;
});
// Presence channel - tracking em tempo real
Broadcast::channel('tracking.{orderId}', function (User $user, int $orderId) {
$order = Order::find($orderId);
return $order && $user->tenant_id === $order->tenant_id
&& $user->can('view', $order);
});
<?php
namespace App\Broadcasting;
use App\Models\Order;
use App\Models\User;
final class OrderChannel
{
public function join(User $user, Order $order): bool
{
return $user->tenant_id === $order->tenant_id
&& $user->can('view', $order);
}
}
// routes/channels.php
Broadcast::channel('orders.{order}', OrderChannel::class);
// resources/js/echo.ts
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 ?? 8081,
wssPort: import.meta.env.VITE_REVERB_PORT ?? 8081,
forceTLS: false,
enabledTransports: ['ws', 'wss'],
});
// Public channel
window.Echo.channel('public-updates')
.listen('.OrderStatusChanged', (e: any) => {
console.log('Order status changed:', e.order);
});
// Private channel
window.Echo.private(`orders.${orderId}`)
.listen('.OrderStatusChanged', (e: any) => {
console.log('Order updated:', e.order);
// Atualizar estado React
setOrder(e.order);
});
// Presence channel - tracking
window.Echo.join(`tracking.${orderId}`)
.here((users: any[]) => {
console.log('Users watching:', users);
})
.joining((user: any) => {
console.log('User joined:', user);
// Mostrar notificação
toast(`${user.name} está acompanhando este pedido`);
})
.leaving((user: any) => {
console.log('User left:', user);
})
.error((error: any) => {
console.error('Presence error:', error);
});
// Notification channel
window.Echo.private(`App.Models.User.${userId}`)
.notification((notification: any) => {
console.log('New notification:', notification);
// Mostrar toast
toast(notification.message);
});
// resources/js/hooks/useEcho.ts
import { useEffect, useState } from 'react';
import window from 'global';
interface UseEchoOptions {
channel: string;
event: string;
type?: 'public' | 'private' | 'presence';
}
export function useEcho({ channel, event, type = 'public' }: UseEchoOptions) {
const [data, setData] = useState<any>(null);
useEffect(() => {
const echo = (window as any).Echo;
if (!echo) return;
let echoChannel;
switch (type) {
case 'private':
echoChannel = echo.private(channel);
break;
case 'presence':
echoChannel = echo.join(channel);
break;
default:
echoChannel = echo.channel(channel);
}
const listener = echoChannel.listen(event, (e: any) => {
setData(e);
});
return () => {
listener.stopListening(channel, event);
};
}, [channel, event, type]);
return data;
}
// Uso
function OrderTracking({ orderId }: { orderId: number }) {
const update = useEcho({
channel: `orders.${orderId}`,
event: '.OrderStatusChanged',
type: 'private',
});
useEffect(() => {
if (update) {
// Atualizar UI
}
}, [update]);
return <div>...</div>;
}
<?php
use App\Notifications\OrderUpdated;
use Illuminate\Support\Facades\Notification;
// Enviar notification em tempo real
Notification::send($users, new OrderUpdated($order));
// Ouvir notifications do usuário atual
window.Echo.private(`App.Models.User.${userId}`)
.notification((notification: any) => {
console.log('Notification:', notification);
// Mostrar toast/badge
});
// Verificar status da conexão
window.Echo.connector.pusher.connection.bind('connected', () => {
console.log('WebSocket connected');
});
window.Echo.connector.pusher.connection.bind('disconnected', () => {
console.log('WebSocket disconnected');
});
window.Echo.connector.pusher.connection.bind('error', (error: any) => {
console.error('WebSocket error:', error);
});
// config/broadcasting.php
'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'),
'url' => env('REVERB_URL'),
'host' => env('REVERB_HOST', '127.0.0.1'),
'port' => env('REVERB_PORT', 8081),
'scheme' => env('REVERB_SCHEME', 'http'),
'options' => [
'cluster' => env('REVERB_CLUSTER', 'mt1'),
'useTLS' => env('REVERB_SCHEME', 'https') === 'https',
'client' => env('REVERB_CLIENT', 'laravel-echo'),
'debug' => env('REVERB_DEBUG', false), // Enable for debug
],
],
],
// Event: TrackingUpdated
public function broadcastOn(): PresenceChannel
{
return new PresenceChannel('tracking.' . $this->order->id);
}
// Frontend
window.Echo.join(`tracking.${orderId}`)
.here((users) => setViewers(users))
.joining((user) => toast(`${user.name} está observando`))
.leaving((user) => removeViewer(user.id));
// Action: UpdateOrderStatusAction
public function handle(Order $order, string $status): Order
{
$oldStatus = $order->status;
$order->update(['status' => $status]);
event(new OrderStatusChanged($order, $oldStatus, $status));
return $order;
}
// Frontend
window.Echo.private(`orders.${orderId}`)
.listen('.OrderStatusChanged', (e) => {
setOrderStatus(e.order.status);
toast(__('messages.order_status_updated'));
});
// Event: NewMessage
public function broadcastOn(): PresenceChannel
{
return new PresenceChannel('chat.' . $this->chat->id);
}
// Frontend
window.Echo.join(`chat.${chatId}`)
.listen('.NewMessage', (e) => {
addMessage(e.message);
});
// Limitar broadcasts por usuário
use Illuminate\Support\Facades\RateLimiter;
RateLimiter::for('broadcast', function (Job $job) {
return Limit::perMinute(100);
});
// Events podem ser queued
final class LargeDataEvent implements ShouldBroadcast
{
use InteractsWithSockets;
use SerializesModels;
public $queue = 'broadcast'; // Fila específica
public $connection = 'redis';
// ...
}
// Sempre validar canais private/presence
Broadcast::channel('orders.{order}', function (User $user, Order $order) {
// Validar tenant ownership
if ($user->tenant_id !== $order->tenant_id) {
return false;
}
// Validar permissões
return $user->can('view', $order);
});
// Não enviar dados sensíveis
public function broadcastWith(): array
{
return [
'id' => $this->order->id,
'status' => $this->order->status,
// Não enviar: user_id, tenant_id, etc.
];
}
Antes de finalizar feature realtime:
laravel-actions-events para dispatch eventslaravel-exceptionslaravel-i18nlaravel-architecture para estrutura