Меню
Laravel Reverb, взаимодействие через WebSockets

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 с первого дня проекта. Это тот редкий случай, когда «родное» решение оказывается одновременно проще и мощнее альтернатив.