Laravel Reverb, взаимодействие через WebSockets
Реальный опыт: как я внедрил Laravel Reverb в свои проекты
Не так давно я столкнулся с задачей — нужно было добавить живой чат поддержки в SaaS-продукт клиента. До этого я использовал Pusher и стороннюю обёртку Laravel WebSockets, но постоянно упирался в лимиты бесплатных тарифов и странные баги с переподключением. Когда Laravel выпустил собственный сервер Reverb, я решил рискнуть и мигрировать на него. Спустя год эксплуатации в продакшене могу сказать честно: это лучшее, что случилось с real-time функциями в Laravel. Делюсь полным гайдом, который прошёл проверку боем — от установки до продакшн-настроек.
Почему Reverb, а не Pusher или Socket.io
Reverb — это нативный WebSocket-сервер, написанный на PHP командой Laravel. Он полностью совместим с протоколом Pusher, что означает: можно использовать привычный Laravel Echo без переписывания фронтенда. Главные плюсы, которые я оценил на практике:
- Работает внутри вашей инфраструктуры, без внешних сервисов.
- Поддерживает горизонтальное масштабирование через Redis.
- Легко запускается и поддерживается через Supervisor или Horizon.
- Никаких лимитов на количество сообщений.
Шаг 1. Установка
Начинаем с чистого проекта Laravel:
composer create-project laravel/laravel realtime-demo
cd realtime-demo
php artisan install:broadcasting
Команда install:broadcasting — это магия. Она автоматически:
- устанавливает пакет laravel/reverb
- публикует конфиг config/reverb.php и config/broadcasting.php
- добавляет нужные переменные в .env
- устанавливает laravel-echo и pusher-js через npm
После установки проверьте .env — там должны появиться такие строки:
BROADCAST_CONNECTION=reverb
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
VITE_REVERB_APP_KEY="${REVERB_APP_KEY}"
VITE_REVERB_HOST="${REVERB_HOST}"
VITE_REVERB_PORT="${REVERB_PORT}"
VITE_REVERB_SCHEME="${REVERB_SCHEME}"
Запускаем сервер Reverb отдельной командой (в проде это делает Supervisor):
php artisan reverb:start
Если увидели Starting server on 0.0.0.0:8080 — всё работает.
Шаг 2. Живой чат — модели и миграции
Создаём таблицу сообщений, команда ниже создает модель и миграцию:
php artisan make:model Message -m
Миграция:
public function up(): void
{
Schema::create('messages', function (Blueprint $table) {
$table->id();
$table->foreignId('user_id')->constrained();
$table->foreignId('chat_id')->constrained();
$table->text('body');
$table->timestamps();
});
}
Модель Message:
class Message extends Model
{
protected $fillable = ['user_id', 'chat_id', 'body'];
public function user(): BelongsTo
{
return $this->belongsTo(User::class);
}
}
Шаг 3. Событие для трансляции
Это ключевая часть — событие, которое будет «выстреливать» в WebSocket-канал:
php artisan make:event MessageSent
class MessageSent implements ShouldBroadcast
{
use Dispatchable, InteractsWithSockets, SerializesModels;
public function __construct(public readonly Message $message)
{
}
public function broadcastOn(): array
{
return [
new PrivateChannel('chat.' . $this->message->chat_id),
];
}
public function broadcastAs(): string
{
return 'message.sent';
}
public function broadcastWith(): array
{
return [
'id' => $this->message->id,
'body' => $this->message->body,
'user' => [
'id' => $this->message->user->id,
'name' => $this->message->user->name,
],
'created_at' => $this->message->created_at->toDateTimeString(),
];
}
}
Важная мелочь для новичков: ShouldBroadcast заставляет Laravel отправить событие через очередь. Очереди позволяют выполнять тяжёлые операции в фоне и не задерживать ответ пользователю.
Обязательно запустите воркер:
php artisan queue:work
Без этого события не будут отправляться — я сам когда-то полчаса дебажил «почему сообщения не приходят», пока не вспомнил про воркер.
Шаг 4. Авторизация приватных каналов
Приватные каналы требуют проверки доступа. Открываем routes/channels.php:
Broadcast::channel('chat.{chatId}', function (User $user, int $chatId) {
$chat = Chat::findOrFail($chatId);
return $chat->participants->contains($user->id);
});
Здесь мы проверяем, что пользователь действительно участник чата — без этого любой мог бы подслушивать чужие переписки.
Шаг 5. Контроллер отправки сообщения
class MessageController extends Controller
{
public function store(Request $request, Chat $chat)
{
$validated = $request->validate([
'body' => 'required|string|max:1000',
]);
$message = $chat->messages()->create([
'user_id' => auth()->id(),
'body' => $validated['body'],
]);
broadcast(new MessageSent($message))->toOthers();
return response()->json($message->load('user'));
}
}
Обратите внимание на ->toOthers() — эта мелочь спасает от дублирования: отправитель уже видит своё сообщение через AJAX-ответ, поэтому событие не нужно отправлять ему же повторно.
Шаг 6. Фронтенд — подписка через Laravel Echo
В resources/js/bootstrap.js уже должна быть настройка Echo после установки broadcasting:
import { Echo } from 'laravel-echo';
import Pusher from 'pusher-js';
window.Pusher = Pusher;
window.Echo = new Echo({
broadcaster: 'reverb',
});
Теперь код подписки на чат, который можно вставить в Vue/React или чистый JS:
Echo.private(`chat.${chatId}`)
.listen('.message.sent', (event) => {
appendMessageToChat(event);
});
function appendMessageToChat(message) {
const container = document.getElementById('messages');
const div = document.createElement('div');
div.classList.add('message');
div.innerHTML = `<strong>${message.user.name}</strong>: ${message.body}`;
container.appendChild(div);
container.scrollTop = container.scrollHeight;
}
Обратите внимание на точку перед message.sent — это обязательно, так как мы явно задали broadcastAs(). Без точки Echo будет искать имя класса, а не наш алиас.
Дашборд в реальном времени
Для интерактивных дашбордов подход тот же, но обычно используется публичный или presence-канал, транслирующий агрегированные метрики. Пример события:
class StatsUpdated implements ShouldBroadcast
{
use Dispatchable, SerializesModels;
public function __construct(public array $stats) {}
public function broadcastOn(): array
{
return [new Channel('dashboard.stats')];
}
}
Отправляем это событие, например, из запланированной задачи (app/Console/Kernel.php или через Task Scheduling в новом стиле), которая раз в 5 секунд собирает свежие данные из базы:
Schedule::call(function () {
$stats = [
'active_users' => User::where('last_seen_at', '>', now()->subMinutes(5))->count(),
'orders_today' => Order::whereDate('created_at', today())->count(),
];
broadcast(new StatsUpdated($stats));
})->everyFiveSeconds();
На фронтенде подписываемся на публичный канал и обновляем виджеты без единой перезагрузки страницы:
Echo.channel('dashboard.stats')
.listen('.StatsUpdated', (event) => {
document.getElementById('active-users').textContent = event.stats.active_users;
document.getElementById('orders-today').textContent = event.stats.orders_today;
});
Система уведомлений
Здесь я использую встроенную поддержку Laravel Notifications с каналом broadcast — это избавляет от написания событий вручную:
class NewOrderNotification extends Notification implements ShouldQueue
{
use Queueable;
public function __construct(public Order $order) {}
public function via($notifiable): array
{
return ['database', 'broadcast'];
}
public function toBroadcast($notifiable): BroadcastMessage
{
return new BroadcastMessage([
'order_id' => $this->order->id,
'message' => "Новый заказ №{$this->order->id}",
]);
}
}
Отправка:
$user->notify(new NewOrderNotification($order));
Laravel автоматически создаёт приватный канал App.Models.User.{id} для каждого пользователя. На фронтенде слушаем так:
Echo.private(`App.Models.User.${userId}`)
.notification((notification) => {
showToast(notification.message);
});
Продакшн: на что обратить внимание
За время эксплуатации я выработал чек-лист для деплоя.
1. Supervisor для Reverb, чтобы сервер не падал навсегда после перезапуска сервера:
[program:reverb]
command=php /var/www/app/artisan reverb:start
autostart=true
autorestart=true
user=www-data
2. Nginx как reverse proxy с поддержкой Upgrade-заголовков для WebSocket:
location /app {
proxy_pass http://127.0.0.1:8080;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
proxy_set_header Host $host;
}
3. Redis для масштабирования — если у вас несколько инстансов Reverb за балансировщиком, обязательно настройте REVERB_SCALING_ENABLED=true и укажите Redis в config/reverb.php.
4. Очереди через Horizon, а не просто queue:work — так проще мониторить задержки в трансляции событий.
Заключение
Laravel Reverb закрыл для меня главную боль real-time функциональности — зависимость от внешних сервисов и их лимитов. За счёт полной совместимости с протоколом Pusher миграция прошла безболезненно: фронтенд почти не пришлось трогать. Если вы делаете чат, дашборд или систему уведомлений на Laravel — не тратьте время на сторонние решения, попробуйте Reverb с первого дня проекта. Это тот редкий случай, когда «родное» решение оказывается одновременно проще и мощнее альтернатив.