Laravel Reverb вышел дней 10 назад, по нему еще не успели наплодить копипастных статей, а по его предшественнику spatie/laravel-websockets совсем мало материала было. Поэтому будет полезно рассмотреть процесс настройки websockets на сайте без сторонних сервисов типа Pusher! Но с уже привычными библиотеками для фронта laravel-echo
и pusher-js
(Только библиотека, без сервиса).
Не буду рассматривать лишние процессы вроде установки проекта, запуска миграций и т.д., а приступлю сразу к делу, при условии, что у вас уже установлен проект с Laravel 11.0.7 (Последняя на момент написания статьи), установлены файлы для API (php artisan install:api
), а также sanctum!
Рассмотрим способ обновления данных по сокетам для заказа пользователем, имеющего доступ к такому заказу.
BACKEND
И так, наш composer.json
выглядит примерно так:
"require": {
"php": "^8.2",
"laravel/framework": "^11.0",
"laravel/sanctum": "^4.0",
"laravel/tinker": "^2.9"
},
"require-dev": {
"fakerphp/faker": "^1.23",
"laravel/pint": "^1.13",
"laravel/sail": "^1.26",
"mockery/mockery": "^1.6",
"nunomaduro/collision": "^8.0",
"phpunit/phpunit": "^10.5",
"spatie/laravel-ignition": "^2.4"
},
Устанавливаем Laravel Reverb
Запускаем команду
php artisan install:broadcasting
На вопрос Would you like to install Laravel Reverb?
Отвечаем - Yes
.
Эта команда установит нам Laravel Reverb и создаст файлы api/channels.php
, config/broadcasting.php
и др. (В ранних версиях Laravel эти файлы были изначально, с 11 версии создается отдельно)
Примечание. Если эти файлы уже были то они не заменятся, можно не волноваться!
Также в .env
файл были добавлены настройки:
REVERB_APP_ID=SOME_GENERATED_VALUE
REVERB_APP_KEY=SOME_GENERATED_VALUE
REVERB_APP_SECRET=SOME_GENERATED_VALUE
REVERB_HOST="localhost"
REVERB_PORT=8080
REVERB_SCHEME=http
На текущем этапе уже можно запустить сервер - php artisan reverb:start
.
Примечание. Если вы обновляли Laravel 10 до Laravel 11 то замените в .env файле ключ BROADCAST_DRIVER на BROADCAST_CONNECTION со знчением reverb т.к. при установке Reverb в конфиге этот ключ был поменян, можно не заметить и долго искать в чем проблема!
Создаем событие
Создаем событие, которое будет транслироваться по сокетам (На примере обновления статуса заказа):
php artisan make:event OrderChanged
У нас создался файл по пути app/Events/OrderChanged.php
со следующим содержимым:
<?php
namespace App\Events;
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;
use Illuminate\Queue\SerializesModels;
class OrderChanged
{
use Dispatchable, InteractsWithSockets, SerializesModels;
/**
* Create a new event instance.
*/
public function __construct()
{
//
}
/**
* Get the channels the event should broadcast on.
*
* @return array<int, \Illuminate\Broadcasting\Channel>
*/
public function broadcastOn(): array
{
return [
new PrivateChannel('channel-name'),
];
}
}
Далее добавляем в этот файл:
- Имплементируем класс от
ShouldBroadcastNow
.
Важно!ShouldBroadcastNow
- предполагает синхронное выполнение события т.е. выполняется сразу. Если у вас уже настроен Redis или другой способ организации очередей то можете использоватьShouldBroadcast
, в противном случае используйтеShouldBroadcastNow
. - Добавляем свойство
public $order;
. Это заказ, который мы будем передавать в событие в нужный момент (Изменение статуса). - Добавляем метод
broadcastWith()
. Это метод, который будет содержать данные для отправки. Если его не указать то класс события автоматически соберет имеющиеся свойства, сериализует и отправит в качесте ответа по сокетам (Поэтому часто используют свойство$message
для чатов и пр.) - Добавляем метод
broadcastOn()
. Указываем канал по которому будем транслировать событие и передавать данные.
В конечном итоге, файл приобретет вид:
<?php
namespace App\Events\Orders;
use App\Models\Orders\Order;
use Illuminate\Broadcasting\InteractsWithSockets;
use Illuminate\Broadcasting\PrivateChannel;
use Illuminate\Contracts\Broadcasting\ShouldBroadcastNow;
use Illuminate\Foundation\Events\Dispatchable;
use Illuminate\Queue\SerializesModels;
class OrderChanged implements ShouldBroadcastNow
{
use Dispatchable, InteractsWithSockets, SerializesModels;
/**
* Экземпляр заказа.
*
* @var Order
*/
public $order;
/**
* Create a new event instance.
*/
public function __construct($order)
{
$this->order = $order;
}
/**
* Получите данные для трансляции.
*
* @return array
*/
public function broadcastWith(): array
{
return ['status' => $this->order->status];
}
public function broadcastOn()
{
return [
new PrivateChannel('orders.' . $this->order->id)
];
}
}
Регистрируем сервис провайдер
php artisan make:provider BroadcastServiceProvider
Меняем его содержимое на следующее:
<?php
namespace App\Providers;
use Illuminate\Support\Facades\Broadcast;
use Illuminate\Support\ServiceProvider;
class BroadcastServiceProvider extends ServiceProvider
{
/**
* Bootstrap services.
*/
public function boot(): void
{
Broadcast::routes(["prefix" => "api", "middleware" => ["auth:sanctum"]]);
require base_path('routes/channels.php');
}
}
Тут мы указываем, что собираемся использовать Broadcasting для API: Добавляем префикс api для маршрута авторизации и добавляем middleware sanctum.
Также регистрируем этот сервис провайдер - добавляем строчку в bootstrap/providers.php
:
Важно. Если вы обновлялись с Laravel 10 то просто расскоментируйте этот сервис провайдер в
config/app.php
если этого не произошло при установке Reverb.
<?php
return [
App\Providers\AppServiceProvider::class,
App\Providers\BroadcastServiceProvider::class,
];
Добавляем канал
Идем в файл routes/channels.php
, который сгенерировался при установке Reverb и добавляем в него канал:
Broadcast::channel('orders.{orderId}', function ($user, $orderId) {
return $user->company_id === Order::find($orderId)->company_id;
});
Что тут происходит:
- {orderId} - это плейсхолдер для номера заказа, который мы передаем из события (Да, тут валидация происходит из бэка на фронт, а не с фронта на бэк, как в случае с api.php).
- $orderId - Параметр, в который подставляется id Заказа из плейсхолдера.
- Внутри callback-функции проводим удобную нам валидацию. Я рассмотрел вариант соответствия id компании заказа и авторизованного пользователя
- $user - Авторизованный пользователь. Сюда он попадет после автоматического запроса laravel-echo на маршрут с auth, который мы формировали в сервис провайдере выше.
Вызов события
Вызываем событие в удобном для нас месте (Там, где будет меняться статус заказа) следующим образом:
\App\Events\OrderChanged::dispatch(Order::find(1)); // Ну или у вас уже будет заказ, у которого меняли статус, в переменной типа $order, ее и прокидываем в параметре
FRONTEND
Установка laravel-echo и pusher-js
Эти две библиотеки используются для подключения к каналам и прослушиванию событий, концепция которых была внедрена в pusher-js.
npm install --save-dev laravel-echo pusher-js
Дальше нужно инициализировать Echo. В зависимости от frontend-фреймворка, который вы используете, производиться это может в разных местах (Во Vue - это index.js,в Quasar - это конфил echo.js в папке boot и добавление в основной конфиг quasar и т.д.), но принцип одинаковый:
import Echo from 'laravel-echo';
import Pusher from 'pusher-js';
window.Pusher = Pusher;
window.Echo = new Echo({
broadcaster: 'reverb',
key: 'YOUR_REVERB_TOKEN',
wsHost: window.location.hostname,
wsPort: 8080,
wssPort: 8080,
forceTLS: false,
enabledTransports: ['ws', 'wss'],
authEndpoint: 'http://YOUR-SITE-DOMAIN.local/api/broadcasting/auth',
auth: {
headers: {
Authorization: `Bearer ${localStorage.token}`
}
}
});
Что здесь особенного:
- key - Ключ вашего приложения, который сгенерировался в .env-файле.
- authEndpoint - Эндпоинт, который мы сформировали в сервис провайдере выше. Конструкция Broadcast::routes() создаем эндпоинт вида broadcasting/auth, н омы к нему еще приделали префикс api, поэтому окончательный вариант тут будет таким -
api/broadcasting/auth
. - auth - Объект с доп. данными для авторизации. Нам тут важно то, что отсюда будет браться
Bearer
токен нашего приложения и прокидываться для авторизации.
Подключение к каналу
Используем нижеприведенный код там, где у нас будет обновление по сокетам:
window.Echo.private('orders.1')
.listen('OrderChanged', (data) => {
message.value = data.message
})
})
На что обратить внимание:
- .private() - Мы прослушиваем Закрытый канал, в котором происходит проверка (Имеем ли мы доступ к заказу)
- orders.1 - Здесь вместо цифры подставляем переменную, в которой будет номер заказа
- .listen() - Метод, который слушает канал, но с проверкой по Namespace (Если мы в файле события не предусмотрели broadcastAs метод). Полное имя события будет такое
App\\Events\\OrderChanged
. Если у нас вложенные в папки события, то соответственно событие нужно указывать безApp\\Events
, например,Orders\\OrderChanged
.
Важно. Если мы указали broadcastsAs() в файле OrderChanged и прописали там, скажем, событие order.changed, то уже тут в методе .listen() добавляем вначале точку, чтобы не учитывались Namespace, вот так - ,listen('.order.changed').
На этом базовая реализация сокетов с приватными каналами рассмотрена!
Весь основной процесс будет происходить так:
- Загружается страница, инциализируются Echo и Pusher
- Echo отправляет запрос на авторизацию используя Bearer токен sanctum
- Устанавливается соединение сокетов на основе проверки в файле
routes/channels.php
, где, также, проходит авторизация - ...Происходят какие то действия в приложении...
- Срабатывает событие
OrderChanged
черезdispatch(Order $order)
. - В файле
OrderChanged
данные из методаbroadcastWith()
отправляются по сокетам на канал с указанием события. - Echo слушает канал, получает данные и передает их приложению для дальнейших действий.
Публичный канал
Для публичного канала, используем в файле события в методе broadcastOn()
вместо PrivateChannel()
- просто Channel()
вот так:
public function broadcastOn()
{
return [
new Channel('public-channel')
];
}
На фронте, в работе Echo используем вместо private()
, просто - channel()
. Вот так:
window.Echo.channel('public-channel')
.listen('.public-event', (data) => {
message.value = data.message
})
})
Для публичного канала, вроде бы, даже не надо его прописывать в routes/channels.php
.
На этом все! Запускаем сервер, если этого еще не делали - php artisan reverb:serve
и можно работать!
Заметки.
- Если что-то не работает - внимательно смотрим конфиги! Я уже ранее упоминал, но тем не менее, если вы обновлялись с Laravel 10, то там должно быть BROADCAST_CONNECTION=reverb, а не BROADCAST_DRIVER=reverb, как это было в ранних версиях Laravel.
- Авторизация Echo с Sanctum происходит автоматически, никаких кастомных контроллеров не нужно, как и маршрутов для авторизации!
- Используем
ShouldBroadcastNow
вместоShouldBroadcast
в файле события если у вас, на текущий момент, не настроены очереди. - Имена каналов по умолчанию с фронта формируются по Namespace события на бэке, причем от корня -
App\\Events
. Если у нас событие лежит в подпапке -App\\Events\\Orders\\Subfolder\\OrderEvent.php
, то используем в Echo часть послеApp\\Events
-.listen('Orders\\Subfolder\\OrderEvent')
. К нему автоматически приклеитсяApp\\Events
. - Если не хотите такие длинные события, используйте
broadcastAs()
метод в файле события, пусть этот метод вернет строку с наименованием события. С фронта не забудьте добавть точку перед названием в методе.listen()
-.listen('.custom.event.name')
, а не.listen('custom.event.name')
. - Если вам не нравится метод
.listen()
со своей магией, можете использовать метод.on()
, он ничего не приклеивает!