В Ларавеле из коробки (ну, почти из коробки) есть поддержка отправки сообщений на вебсокетах, что бывает довольно полезно. Например, когда мы из фронта просим сервер выполнить длительную операцию, и не можем, или не хотим ждать её окончания. А вот узнать, когда она закончится — наоборот, очень хотим. Для чего нам нужно получить от сервера обратную связь. В эру палеолита для этого каждый клиент опрашивал сервер раз в секунду до получения ответа (или до полного изумления сервера). Но теперь у нас есть вебсокеты, с ними проще. Можно передать с сервера результат отложенной операции, можно сделать уведомления о действиях других юзеров, или просто чатик сделать даже. Почитать про это можно в документации на Laravel Broadcasting.
Что особенно радует, этот broadcasting так клёво устроен, что один раз его настроив, достаточно добавить классу события (Event) реализацию метода broadcastOn для указания, в какие каналы должно улетать сообщение:
class PDFGenerated implements ShouldBroadcast
{
use Dispatchable, InteractsWithSockets, SerializesModels;
public function __construct(public Document $document, public $source_id) {}
public function broadcastOn(): array
{
return [
new PrivateChannel('orders.' . $this->document->order_id),
];
}
}
и после этого любой вызов PDFGenerated::dispatch($document, $source_id) будет автоматически отправлять событие через вебсокеты в нужный канал.
И достаточно написать:
this.channel = window.Echo.private('orders.' + order.id)
.listen('PDFGenerated', (e) => {
this.insertDocument(e.document);
});
чтобы подписаться на канал orders.id и реагировать на событие PDFGenerated, в котором заодно и данные передаются. Причем этот код будет работать и для vue, и для реакта, и для голого js.
В общем, использовать броадкастинг гораздо проще и быстрее, чем даже его настроить. Так что однажды настроив — сразу хочется разгуляться. Но сначала все же надо настроить, об этом и пост. Потому что конфигурация броадкастинга и конкретно reverb-сервера разделена по двум страницам, и некоторые нюансы там не упоминаются.
Итак, у меня есть локальный докер-контейнер с LAMP внутри. Настроим его на работу с Laravel Reverb (бесплатным сервером, реализующим push-протокол для обменов по вебсокетам). В доках Ларавела есть инструкция, но ниже мы рассмотрим некоторые моменты подробнее.
Для начала нам нужно, конечно, всё установить. Для бэка:
php artisan install:broadcasting --reverb
для фронта:
npm install --save-dev laravel-echo pusher-js
Затем пройти в resourses/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 ?? 80,
wssPort: import.meta.env.VITE_REVERB_PORT ?? 443,
forceTLS: (import.meta.env.VITE_REVERB_SCHEME ?? 'https') === 'https',
enabledTransports: ['ws', 'wss'],
});
и собрать фронт npm run build. Значения VITE_REVERB_APP_KEY и остальные будут собраны из вашего .env-файла. Обратите внимание, что если у вас не vite, а mix, то данные из .env пробрасываются немного иначе:
window.Echo = new Echo({
broadcaster: 'reverb',
key: process.env.MIX_REVERB_APP_KEY,
wsHost: process.env.MIX_REVERB_HOST,
wsPort: process.env.MIX_REVERB_PORT,
wssPort: process.env.MIX_REVERB_PORT,
forceTLS: (process.env.MIX_REVERB_SCHEME ?? 'https') === 'https',
enabledTransports: ['ws', 'wss'],
});
у меня именно так, поскольку проект прошёл путь от Laravel 5 до Laravel 12, и mix на vite я по пути не менял.
Теперь посмотрим в сам .env:
REVERB_APP_ID=123456
REVERB_APP_KEY=abcdefghijklmnopqrstu
REVERB_APP_SECRET=vwxyz1234567890
REVERB_HOST=172.17.0.2
REVERB_PORT=443
REVERB_SCHEME=https
REVERB_SERVER_PORT=0.0.0.0
REVERB_SERVER_PORT=8090
REVERB_CLIENT_VERIFY_SSL=false
# для передачи во фронт
MIX_REVERB_APP_KEY="${REVERB_APP_KEY}"
MIX_REVERB_HOST="${REVERB_HOST}"
MIX_REVERB_PORT="${REVERB_PORT}"
MIX_REVERB_SCHEME="${REVERB_SCHEME}"
Первые три переменные вам скорее всего уже сгенерировал инсталлятор. Но при запуске в продакшне их стоит перегенерировать отдельно, можно это сделать например так:
echo "REVERB_APP_ID=$(openssl rand -hex 8)" >> .env
echo "REVERB_APP_KEY=$(openssl rand -hex 16)" >> .env
echo "REVERB_APP_SECRET=$(openssl rand -hex 32)" >> .env
Далее, REVERB_HOST и REVERB_PORT — это хост и порт, куда будет подсоединяться клиентский браузер. Для локальной разработки у меня это IP докер-контейнера (172.17.0.2), но может быть и хост если контейнер поименован. Ну а для продакшна — ваш домен. Порт, соответственно, 443 для https и 80 для http (см. далее).
Далее нужно указать протокол http или https и тут всё просто: если вы в локальной разработке используете https с самоподписанными сертификатами (а я всегда так делаю), то всегда указывайте https. Иначе политика безопасности браузера не позволит соединению произойти.
Далее мы указываем REVERB_SERVER_HOST и REVERB_SERVER_PORT. Это хост и порт, на котором работает сам reverb на сервере. На этот хост и порт ваш веб-сервер должен пробросить вебсокет-соединение.
Обратите внимание, что у меня стоит порт 8090. По умолчанию reverb использует 8080, но тот же порт по умолчанию использует связка nginx+apache с пробросом входящих http и https соединений на порт 8080 апача. Такую конфигурацию вы можете получить на сервере автоматически, если, например, установите панель ispmanager.
Форвардинг вебсокет-соединения нужно ещё конечно настроить. Если у вас на входе nginx, добавьте в конфиг:
location /app {
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:8090;
}
/app — это путь по умолчанию, на который Reverb принимает соединения.
Если же у вас на входе апач (как у меня во многих локальных версиях проектов), то для форвардинга можно добавить:
ProxyPreserveHost On
RewriteEngine On
RewriteCond %{HTTP:Upgrade} websocket [NC]
RewriteCond %{HTTP:Connection} upgrade [NC]
RewriteRule /app/(.*) ws://localhost:8090/app/$1 [P,L]
ProxyPass /app http://localhost:8090/app
ProxyPassReverse /app http://localhost:8090/app
Наконец, мы в .env видем вот такую переменную: REVERB_CLIENT_VERIFY_SSL=false. Это дополнение от меня, полезное для локальной разработки. Как я уже говорил, я локально всегда использую https с самоподписанными сертификатами. Но библиотека Guzzle, которая используется для установки соединения, таких вольностей по умолчанию не разрешает. Поэтому чтобы работало, нужно в библиотеке отключить проверку SSL. Но только локально, поэтому в конфиге config/broadcasting.php мы добавляем client_options — эти параметры передадутся в Guzzle Client и true по умолчанию позволит сохранить проверку SSL на продакшне:
'reverb' => [
'driver' => 'reverb',
'key' => env('REVERB_APP_KEY'),
'secret' => env('REVERB_APP_SECRET'),
'app_id' => env('REVERB_APP_ID'),
'options' => [
'host' => env('REVERB_HOST', 'localhost'),
'port' => env('REVERB_PORT', 8080),
'scheme' => env('REVERB_SCHEME', 'http'),
'useTLS' => env('REVERB_SCHEME', 'https') === 'https',
],
'client_options' => [
'verify' => env('REVERB_CLIENT_VERIFY_SSL', true),
],
],
Ну а теперь мы можем перезапустить сервер (чтобы настройки проброса портов схватились) и запустить Reverb:
php artisan reverb:start --debug
локально ключик --debug позволит видеть в консоли все сообщения. Не забудьте, что броадкастинг событий происходит через очередь, поэтому она у вас должна быть либо настроена, либо QUEUE_DRIVER=sync для синхронного выполнения задач. Ну а на продакшне запускайте Reverb без ключика --debug, но через Supervisor. И не забывайте перезапускать через reverb:restart, если внесли изменения в код.
О каких ещё нюансах следует помнить.
Во-первых, когда мы собираем фронт-энд, наши переменные типа MIX_REVERB_APP_KEY (или VITE_REVERB_APP_KEY) вставляются в скрипты при сборке. Поэтому если мы собираем фронт под продакшн локально, а не на продакшне или в CI/CD пайплайне, нужно не забыть при сборке указать переменные окружения от продакшна. Иначе фронт будет пытаться установить соединение с локальными настройками.
И во-вторых, если вы пишете тесты, то как только вы настроите броадкастинг, вызов при запуске тестов MyEvent::dispatch() для события, которое должно быть отправлено в указанные каналы, повлечет ошибку. Поэтому имеет смысл в такой ситуации добавлять мокинг Event::fake() с последующим Event::assertDispatched() для проверки, что событие было отправлено. Например, так:
#[Test]
public function when_pdf_is_generated_it_dispatches_pdf_generated_event()
{
$document = Document::factory()->create([
'path' => 'documents/contracts/generic.docx',
]);
Event::fake();
$response = $this->json('POST', '/api/documents/' . $document->id . '/pdf');
$response->assertStatus(200);
Event::assertDispatched(function (PDFGenerated $event) use ($document) {
return ($document->name == $event->document->name) &&
(str_replace('.docx', '.pdf', $document->original_name) == $event->document->original_name) &&
(str_replace('.docx', '.pdf', $document->path) == $event->document->path) &&
('application/pdf' == $event->document->mime_type);
});
}
Ну а если вы не пишете тесты, то почему собственно вы их не пишете? Пора писать.

