В первой части мы разобрались, что Real-time на Laravel-сайте нужен там, где интерфейс должен получать изменения без перезагрузки страницы: новые уведомления, смену статуса заказа, сообщения в чате, обновления виджетов, события в административной панели. Для таких задач классическая модель HTTP-запроса уже недостаточна, а polling создаёт лишнюю нагрузку на backend. Один из практичных вариантов решения — использовать Centrifugo как отдельный WebSocket-сервер рядом с Laravel-приложением.
В этой статье разберём архитектуру Laravel + Centrifugo: за что отвечает Laravel, какую роль выполняет Centrifugo, как frontend подключается к real-time каналу и как выглядит типовой сценарий публикации события, например при изменении статуса заказа.
Зачем разделять Laravel и Centrifugo
Laravel остаётся основным backend-приложением. Он принимает HTTP-запросы, обрабатывает бизнес-логику, проверяет права доступа, работает с базой данных, запускает очереди и формирует события приложения. Именно Laravel должен решать, что произошло в системе и кто имеет право это увидеть.
Centrifugo выполняет другую задачу. Он отвечает за WebSocket-соединения, каналы, подписки и доставку сообщений клиентам. Это не замена Laravel и не второй backend с бизнес-логикой. Centrifugo — real-time транспортный слой, который получает событие от Laravel и доставляет его подписчикам нужного канала.
Такое разделение особенно важно для поддержки проекта в будущем. Если смешать бизнес-логику, авторизацию и доставку WebSocket-сообщений в одном месте, система быстро станет хрупкой. На демо это выглядит бодро. В production потом начинается привычная археология: почему пользователь получил не своё событие, почему статус пришёл раньше сохранения в базе, почему frontend обновился, а данные в API ещё старые.
Правильная архитектура Laravel Centrifugo строится на простой идее: Laravel является источником истины, а Centrifugo — механизмом доставки изменений в реальном времени.
Общая схема архитектуры Laravel + Centrifugo
В real-time архитектуре сайта участвуют три основных слоя:
-
Laravel backend — бизнес-логика, права доступа, база данных, события, очереди.
-
Centrifugo server — WebSocket-соединения, каналы, подписки, доставка сообщений.
-
Frontend client — подключение к Centrifugo, подписка на каналы, обновление интерфейса.
Здесь важно понимать роль каждого слоя. HTTP остаётся основой для загрузки данных, выполнения команд и восстановления состояния. WebSocket через Centrifugo используется для мгновенной доставки изменений.
Например, страница заказа может сначала загрузить данные обычным HTTP-запросом:
{ "id": 7821, "status": "pending", "amount": 1500}
После этого frontend подписывается на real-time канал пользователя. Когда заказ будет оплачен, Laravel отправит событие в Centrifugo, а Centrifugo доставит его браузеру через WebSocket.
Laravel отвечает за бизнес-логику и события
Laravel не должен просто «проксировать» сообщения в WebSocket. Его задача глубже. Backend должен выполнить действие, проверить права, изменить состояние системы и только после этого создать событие.
Рассмотрим пример с оплатой заказа. Платёжная система отправляет webhook. Laravel проверяет входящие данные, находит заказ, меняет его статус и создаёт событие OrderStatusChanged.
Упрощённый пример контроллера:
namespace App\Http\Controllers\Payment;use App\Events\OrderStatusChanged;use App\Models\Order;use Illuminate\Http\JsonResponse;use Illuminate\Http\Request;class PaymentWebhookController{ public function __invoke(Request $request): JsonResponse { $order = Order::query() ->where('payment_id', $request->input(key: 'payment_id')) ->firstOrFail(); $order->update([ 'status' => 'paid', ]); event(new OrderStatusChanged( orderId: $order->id, userId: $order->user_id, status: 'paid', )); return response()->json(data: [ 'success' => true, ]); }}
В реальном проекте здесь должны быть проверка подписи webhook, идемпотентность, транзакции, защита от повторной обработки и корректная обработка ошибок. Но архитектурный принцип уже виден: сначала Laravel меняет состояние данных, затем создаёт событие.
Само событие можно оформить отдельным классом:
namespace App\Events;class OrderStatusChanged{ public function __construct( public readonly int $orderId, public readonly int $userId, public readonly string $status, ) { }}
Такой класс не должен знать ничего о Centrifugo. Это важная деталь. Событие описывает факт предметной области: статус заказа изменился. А куда потом этот факт уйдёт — в Centrifugo, email, лог аудита или аналитику — решают отдельные обработчики.
Публикация событий в Centrifugo через очередь
Публиковать событие в Centrifugo прямо из контроллера технически возможно, но архитектурно некорректно. Контроллер должен отвечать за HTTP-вход, а не за доставку real-time сообщений. Лучше использовать Laravel Events, listeners и queue jobs.
Listener может отправлять задачу в очередь:
namespace App\Listeners;use App\Events\OrderStatusChanged;use App\Jobs\PublishOrderStatusChangedToRealtime;class SendOrderStatusChangedToRealtime{ public function handle(OrderStatusChanged $event): void { PublishOrderStatusChangedToRealtime::dispatch( orderId: $event->orderId, userId: $event->userId, status: $event->status, )->afterCommit(); }}
Метод afterCommit() здесь принципиален. Real-time событие не должно уйти клиенту раньше, чем новое состояние будет сохранено в базе данных. Иначе пользователь может получить сообщение о статусе paid, обновить интерфейс, затем запросить заказ через API и увидеть старый статус pending. После этого начинается поиск «плавающего бага», хотя причина обычно банальна: событие было отправлено слишком рано.
Job публикации может выглядеть так:
namespace App\Jobs;use App\Services\Realtime\CentrifugoPublisher;use Illuminate\Contracts\Queue\ShouldQueue;class PublishOrderStatusChangedToRealtime implements ShouldQueue{ public function __construct( public readonly int $orderId, public readonly int $userId, public readonly string $status, ) { } public function handle(CentrifugoPublisher $publisher): void { $publisher->publish( channel: '$users:' . $this->userId, data: [ 'type' => 'order.status.changed', 'orderId' => $this->orderId, 'status' => $this->status, ], ); }}
Такой подход даёт несколько преимуществ. Основной HTTP-запрос не зависит напрямую от доступности Centrifugo. Публикацию можно повторить при временной ошибке. Логику доставки проще тестировать. В будущем можно добавить retry, метрики, отдельные логи и разные типы real-time событий.
Centrifugo отвечает за WebSocket, каналы и доставку сообщений
Centrifugo в этой архитектуре не должен принимать бизнес-решения. Он не знает, почему заказ стал оплаченным, кто оплатил счёт и какие правила действуют для пользователя. Его задача — доставить сообщение в канал.
Канал — центральная сущность real-time обмена. Frontend подписывается на канал, а backend публикует туда сообщения.
Примеры каналов:
$users:15$orders:7821$admin:orderschat:room:45dashboard:payments
Для персональных данных обычно используют приватные каналы. Например, канал $users:15 предназначен для событий конкретного пользователя. В него можно отправлять уведомления, изменения статусов заказов, результаты фоновых задач и другие индивидуальные события.
Laravel может публиковать сообщения в Centrifugo через отдельный сервис:
namespace App\Services\Realtime;use Illuminate\Support\Facades\Http;class CentrifugoPublisher{ public function publish( string $channel, array $data, ): void { Http::withHeaders(headers: [ 'Authorization' => 'apikey ' . config('services.centrifugo.api_key'), ])->post( url: config('services.centrifugo.api_url') . '/api/publish', data: [ 'channel' => $channel, 'data' => $data, ], )->throw(); }}
Сервис публикации лучше держать в одном месте. Не нужно размазывать HTTP-вызовы Centrifugo по контроллерам, моделям и listener-ам. Один сервис проще заменить, расширить и покрыть тестами.
Отдельно стоит следить за payload. В WebSocket-событие не нужно отправлять всю Eloquent-модель. Это риск утечки лишних данных и источник нестабильности контракта между backend и frontend.
Лучше отправлять минимальное событие:
{ "type": "order.status.changed", "orderId": 7821, "status": "paid"}
Если frontend нужны дополнительные данные, он может запросить актуальное состояние через обычный HTTP API. Real-time сообщает об изменении, HTTP восстанавливает полную картину.
Frontend подключается к Centrifugo и обновляет интерфейс
Frontend получает начальное состояние от Laravel, затем подключается к Centrifugo и подписывается на нужные каналы. После получения публикации он обновляет интерфейс.
Упрощённый пример подключения:
import { Centrifuge } from 'centrifuge';const centrifuge = new Centrifuge('wss://example.com/connection/websocket', { getToken: async function () { const response = await fetch('/realtime/connection-token', { headers: { Accept: 'application/json', }, credentials: 'same-origin', }); const data = await response.json(); return data.token; },});const userChannel = '$users:15';const subscription = centrifuge.newSubscription(userChannel, { getToken: async function () { const response = await fetch('/realtime/subscription-token', { method: 'POST', headers: { Accept: 'application/json', 'Content-Type': 'application/json', }, credentials: 'same-origin', body: JSON.stringify({ channel: userChannel, }), }); const data = await response.json(); return data.token; },});subscription.on('publication', function (context) { if (context.data.type === 'order.status.changed') { updateOrderStatus( context.data.orderId, context.data.status, ); }});subscription.subscribe();centrifuge.connect();
Этот пример показывает базовый принцип, но в production нельзя доверять имени канала, которое пришло с клиента. Пользователь может изменить '$users:15' на '$users:16'. Поэтому Laravel обязан проверять право подписки и выдавать token только на разрешённый канал.
Пример сценария: Laravel меняет статус заказа и публикует событие
Соберём весь процесс в один пример.
Пользователь открыл страницу заказа. Laravel отдал начальные данные по HTTP. Заказ находится в статусе pending. Frontend подключился к Centrifugo и подписался на канал пользователя $users:15.
Потом платёжная система отправила webhook. Laravel проверил запрос, нашёл заказ, изменил статус на paid, сохранил новое состояние в базе данных и создал событие OrderStatusChanged.
Listener отправил задачу в очередь после commit транзакции. Job вызвал CentrifugoPublisher и опубликовал сообщение:
{ "channel": "$users:15", "data": { "type": "order.status.changed", "orderId": 7821, "status": "paid" }}
Centrifugo доставил сообщение всем активным подключениям, подписанным на канал $users:15. Если у пользователя открыто несколько вкладок, обновление могут получить все вкладки. Frontend обработал событие и изменил статус заказа на странице с «Ожидает оплаты» на «Оплачен».
Архитектурная цепочка получается такой:
Практические правила для устойчивой архитектуры
Чтобы real-time Laravel не превратился в набор случайных WebSocket-сообщений, стоит придерживаться нескольких правил.
-
Laravel должен оставаться источником истины. Все изменения состояния должны фиксироваться в базе данных или другом основном хранилище. WebSocket-событие не должно быть единственным доказательством того, что что-то произошло.
-
Centrifugo должен оставаться транспортом. В нём не нужно размещать бизнес-логику, сложную авторизацию или правила предметной области.
-
Публиковать события лучше после commit транзакции. Это снижает риск рассинхронизации между интерфейсом и API.
-
Payload должен быть минимальным и стабильным. Полная Eloquent-модель в real-time событии — плохая идея. Лучше передавать тип события, идентификатор сущности и несколько необходимых полей.
-
Frontend должен уметь восстановить состояние через HTTP. Если пользователь потерял соединение, закрыл ноутбук, вернулся из спящего режима или пропустил событие, интерфейс должен запросить актуальные данные у Laravel.
Заключение
Архитектура Laravel + Centrifugo строится на ясном разделении ответственности. Laravel отвечает за бизнес-логику, права доступа, события, очереди и состояние данных. Centrifugo отвечает за WebSocket-соединения, каналы, подписки и доставку сообщений. Frontend подключается к Centrifugo, получает публикации и обновляет интерфейс в реальном времени.
Такой подход позволяет добавить real-time обновления на сайт без разрушения backend-архитектуры. Laravel остаётся главным приложением и источником истины, а Centrifugo становится специализированным real-time слоем для доставки событий.
Для Laravel-проектов это особенно удобно: можно использовать привычные Events, listeners, jobs, очереди, конфигурацию и сервисный слой. В результате real-time становится не отдельной игрушкой сбоку, а нормальной частью web-архитектуры.
ссылка на оригинал статьи https://habr.com/ru/articles/1035046/