Руководство по использованию Websockets в Laravel

Источник: «A Guide to Using Websockets in Laravel»
Узнайте, как можно использовать WebSockets и Pusher для добавления функциональности реального времени в приложения Laravel.

Многие веб-сайты и веб-приложения, с которыми вы ежедневно взаимодействуете, предоставляют информацию в режиме реального времени. Например, при использовании приложения для обмена сообщениями сообщения появляются на экране сразу после их отправки, не требуя обновления страницы в браузере.

Как правило, подобная функциональность реального времени реализуется в веб-приложениях с помощью WebSockets.

В этой статье мы рассмотрим, что такое WebSockets, где они могут понадобиться, а также альтернативные подходы к использованию WebSockets. Затем мы расскажем, как реализовать WebSockets в приложениях Laravel с помощью Pusher. Рассмотрим, как настроить бэкенд на отправку широковещательных сообщений через WebSockets, а также как настроить фронтенд на прослушивание этих сообщений. Наконец, рассмотрим использование приватных каналов, каналов присутствия, классов каналов и клиентских событий.

Что такое WebSockets

WebSockets — это технология, позволяющая передавать и получать данные между клиентом и сервером в режиме реального времени. Они позволяют передавать данные с сервера на клиент без необходимости запроса со стороны клиента. Таким образом, вы можете отправлять данные пользователям, как только они становятся доступными, без необходимости обновлять страницу в браузере.

WebSockets полезны для реализации функций, требующих мгновенной обратной связи без какого-либо интервала, например, уведомлений в реальном времени и чат-приложений.

Прежде чем углубиться в эту тему, важно понять два ключевых понятия, используемых при работе с WebSockets: события и каналы.

Каналы можно рассматривать как способ группировки событий. Например, в чат-приложении для каждого чата может быть создан свой канал, например chat.1 и chat.2 (где 1 и 2 — идентификаторы чатов). На эти каналы подписываются (или присоединяются) пользователи чатов, и только события, связанные с этими конкретными чатами (например, отправка сообщения, присоединение или уход пользователя), транслируются пользователям.

События — это фактические данные, передаваемые пользователям на канале. Например, в приложении чата можно иметь события для момента отправки сообщения (содержащие само сообщение), для момента присоединения пользователя к чату и для момента выхода пользователя из чата.

Как правило, пользователь подписывается на канал и прослушивает события.

Чтобы привести пример использования WebSockets в приложении, представим, что у вас есть кнопка, которая формирует отчёт, а затем предоставляет ссылку для его загрузки. Предположим, что на формирование отчёта иногда уходит несколько минут, поэтому не стоит ждать, пока отчёт будет сформирован, прежде чем приложение позволит пользователю сделать что-то ещё. Вместо этого нажатие кнопки может инициировать построение отчёта с помощью очереди. После создания отчёта приложение может передать пользователю событие через WebSockets, чтобы на экране появилось окно с уведомлением, что отчёт готов к загрузке.

Выбор WebSockets-сервера

Как правило, для передачи данных клиенту с помощью WebSockets необходимо использовать сервер WebSockets, способный обрабатывать соединения между клиентом и сервером. В зависимости от конкретного приложения, вы можете самостоятельно настроить такие серверы, используя что-то вроде laravel-websockets или Soketi. Однако иногда это может привести к дополнительному усложнению инфраструктуры системы, поскольку необходимо самостоятельно поддерживать и управлять сервером.

Вместо этого можно использовать управляемые сервисы, такие как Pusher или Ably. С помощью этих сервисов можно организовать передачу данных пользователям и сосредоточиться на создании приложения.

Преимущество использования управляемых сервисов, таких как Pusher, заключается в том, что вам не нужно беспокоиться о сложности обслуживания собственного сервера WebSockets. Однако это означает, что вам придётся отправлять данные третьей стороне, что иногда может представлять собой проблему безопасности в зависимости от типа отправляемых данных. К счастью, Pusher позволяет включить сквозное шифрование для WebSockets, так что прочитать данные сможет только тот, кому они предназначены.

Как вы понимаете, в каждом конкретном проекте вам придётся решать, что лучше — использовать управляемый сервис или создать собственный сервер WebSockets. В рамках данного руководства мы будем использовать Pusher. Однако общие принципы, которые мы рассмотрим, могут быть реализованы и с помощью других сервисов WebSocket.

WebSockets vs. polling

Как мы уже говорили, WebSockets позволяют отправлять данные пользователю в реальном времени при наступлении того или иного события. Поэтому при таком подходе данные отправляются пользователю только тогда, когда они доступны.

Однако аналогичные подходы могут быть реализованы и без использования WebSockets, например, polling/опрос. Под опросом понимается регулярная отправка запроса от браузера клиента к серверу через заданный интервал времени для получения новой копии данных. Например, каждые пять секунд можно посылать запрос на сервер, чтобы проверить, есть ли новые уведомления для отображения пользователю. Таким образом, с его помощью можно обеспечить работу в режиме, близком к реальному времени.

Чтобы сравнить эти два подхода, рассмотрим несколько примеров.

Для начала представим, что у нас есть приложение для чата. В этом приложении, как только сообщение отправлено, мы хотим отобразить его в браузере получателя. Если используются WebSockets, то от сервера к браузеру получателя идёт одна передача, и сообщение отображается мгновенно. Однако если используется опрос, а браузер настроен на опрос каждые пять секунд, то возможна задержка до пяти секунд, прежде чем сообщение будет показано пользователю. Конечно, в данном случае опрос не очень подходит, поскольку пользователю может быть неприятно ждать пять секунд до получения сообщения.

Во-вторых, представим, что у нас есть приборная панель, отображающая некоторые показатели продаж и графики для вашего бизнеса. Опять, при использовании WebSockets, как только происходит продажа, панель обновляется мгновенно. Однако если используется опрос, то приборная панель обновляется через пять секунд. В этом сценарии показатели продаж на приборной панели не так чувствительны к времени, как в чат-приложении, поэтому не так важно, чтобы приборная панель обновлялась мгновенно. Поэтому в данном случае, хотя WebSockets и не помешал бы, вполне подойдёт и опрос. Более того, можно даже увеличить интервал опроса до тридцати секунд или одной минуты, чтобы снизить нагрузку на сервер.

Важно помнить, что WebSockets отправляет данные только тогда, когда они доступны, поэтому если новых данных нет, то нет необходимости обрабатывать их на сервере и отправлять в браузер. Однако опрос может создавать дополнительную нагрузку на сервер, постоянно посылая запросы на проверку наличия новых данных.

Поэтому в каждом конкретном случае необходимо решать, использовать ли WebSockets или альтернативный подход, например, опрос.

Теперь, когда мы рассмотрели, что такое WebSockets, давайте посмотрим, как можно использовать их в Laravel-приложениях с помощью Pusher.

Настройка Pusher

Чтобы начать работу с Pusher, сначала необходимо создать учётную запись. Это можно сделать на сайте Pusher.

После регистрации необходимо создать новое приложение Channels. В рамках данной статьи мы будем использовать следующие данные для создаваемого приложения:

Вы также можете установить флажок Создать приложения для нескольких сред?, который позволяет создать отдельные приложения для локальной, тестовой и производственной сред. Это может быть полезно, если вы хотите использовать отдельные приложения Pusher, чтобы ваши данные были полностью разделены для каждой среды. Однако для целей данного руководства мы будем использовать только одно приложение.

После создания приложения можно кликнуть на навигационной опции App Keys, чтобы просмотреть ключи, которые необходимо добавить в приложение Laravel.

Настройка бэкенда

Теперь, когда мы создали приложение в Pusher, давайте рассмотрим, как мы можем использовать его в приложении Laravel.

Для регистрации функций авторизации вещания в приложении Laravel поставляется с провайдером App\Providers\BroadcastServiceProvider. По умолчанию этот провайдер не регистрируется автоматически в новых приложениях Laravel. Поэтому необходимо перейти в файл config/app.php и раскомментировать строку, содержащую провайдер App\Providers\BroadcastServiceProvider::class.

Также необходимо убедиться, что запущена рабочая очередь для обработки широковещательных событий. По умолчанию Laravel обрабатывает все широковещательные события в очереди, чтобы предотвратить возврат ответов на запросы пользователей.

Поскольку в этой статье мы используем Pusher Channels, нам также потребуется установить Pusher Channels PHP SDK. Вы можете установить его с помощью Composer, выполнив следующую команду в терминале:

composer require pusher/pusher-php-server

В зависимости от того, какой сервис WebSocket вы используете, вам может потребоваться установить другой пакет или не устанавливать его вовсе. Например, если вы используете Ably, то вам необходимо установить Ably PHP SDK.

Затем вы можете добавить ключи, полученные от Pusher, в файл .env вашего проекта:

PUSHER_APP_ID=pusher-app-id-goes-here
PUSHER_APP_KEY=pusher-key-goes-here
PUSHER_APP_SECRET=pusher-secret-goes-here
PUSHER_APP_CLUSTER=pusher-cluster-goes-here

Если вы используете стандартный файл Laravel .env, то, возможно, заметили следующие значения:

VITE_PUSHER_APP_KEY="${PUSHER_APP_KEY}"
VITE_PUSHER_HOST="${PUSHER_HOST}"
VITE_PUSHER_PORT="${PUSHER_PORT}"
VITE_PUSHER_SCHEME="${PUSHER_SCHEME}"
VITE_PUSHER_APP_CLUSTER="${PUSHER_APP_CLUSTER}"

Переменные окружения, начинающиеся с VITE_, используются Vite для того, чтобы мы могли обращаться к их значениям в JavaScript-коде. Например, к VITE_PUSHER_APP_KEY можно получить доступ в JavaScript-коде с помощью import.meta.env.VITE_PUSHER_APP_KEY. Если этих переменных окружения ещё нет в вашем файле .env (и вы используете Vite для компиляции вашего JavaScript), их можно добавить. Такой подход позволяет хранить ключи в одном месте (в файле .env) и не добавлять в код приложения жёстко заданные значения. Это облегчает обслуживание, если в будущем при ротации ключей потребуется изменить значения, например, в случае взлома.

Важно помнить, что не следует делать переменную PUSHER_APP_SECRET доступной для фронтенда с помощью одной из переменных VITE_. Она используется исключительно на бэкенде и никогда не должна быть доступна фронтенду.

Также необходимо обновить файл .env, установив для параметра BROADCAST_DRIVER значение pusher:

BROADCAST_DRIVER=pusher

Все эти переменные окружения используются внутри конфигурационного файла config/broadcasting.php. Поэтому, если вы хотите внести какие-либо изменения в конфигурацию, связанную с вещанием, вы можете изменить их в этом файле.

Теперь бэкэнд должен быть настроен и готов к отправке широковещательных сообщений через WebSockets. Давайте рассмотрим, как настроить фронтенд.

Настройка фронтенда

Laravel предоставляет удобную библиотеку JavaScript, которую можно использовать для подписки на каналы WebSocket и прослушивания событий, под названием Laravel Echo. Laravel Echo удобна тем, что предоставляет простой API, который можно использовать для подписки на каналы без необходимости понимать основной протокол WebSocket.

Вы можете установить его в свой проект с помощью NPM, выполнив в терминале следующую команду:

npm install --save-dev laravel-echo pusher-js

Вы можете заметить, что мы также устанавливаем библиотеку pusher-js. Её установка необходима, поскольку мы используем Pusher для трансляции событий.

После установки Laravel Echo мы можем создать его новый экземпляр в нашем JavaScript-коде.

В данном примере мы будем считать, что для создания активов нашего приложения мы используем Vite, поэтому мы можем получить доступ к переменным окружения, которые мы добавили в наш файл .env ранее. Если вы используете другой инструмент, например Webpack (напрямую или через Laravel Mix), то вам может потребоваться другой подход для доступа к переменным окружения.

Если вы работаете над новым приложением Laravel, то файл resources/js/bootstrap.js, поставляемый с установкой по умолчанию, уже содержит код для работы с Laravel Echo. Однако необходимо не забыть раскомментировать его, чтобы он мог быть использован. Если вы работаете не со свежим приложением Laravel, то содержимое файла resources/js/bootstrap.js выглядит следующим образом:

/**
* We'll load the axios HTTP library which allows us to easily issue requests
* to our Laravel back-end. This library automatically handles sending the
* CSRF token as a header based on the value of the "XSRF" token cookie.
*/


import axios from 'axios';
window.axios = axios;

window.axios.defaults.headers.common['X-Requested-With'] = 'XMLHttpRequest';

/**
* Echo exposes an expressive API for subscribing to channels and listening
* for events that are broadcast by Laravel. Echo and event broadcasting
* allows your team to easily build robust real-time web applications.
*/


import Echo from 'laravel-echo';

import Pusher from 'pusher-js';
window.Pusher = Pusher;

window.Echo = new Echo({
broadcaster: 'pusher',
key: import.meta.env.VITE_PUSHER_APP_KEY,
cluster: import.meta.env.VITE_PUSHER_APP_CLUSTER ?? 'mt1',
wsHost: import.meta.env.VITE_PUSHER_HOST ? import.meta.env.VITE_PUSHER_HOST : `ws-${import.meta.env.VITE_PUSHER_APP_CLUSTER}.pusher.com`,
wsPort: import.meta.env.VITE_PUSHER_PORT ?? 80,
wssPort: import.meta.env.VITE_PUSHER_PORT ?? 443,
forceTLS: (import.meta.env.VITE_PUSHER_SCHEME ?? 'https') === 'https',
enabledTransports: ['ws', 'wss'],
});

Как вы могли заметить, мы работаем с переменными окружения VITE_PUSHER_* (такими как VITE_PUSHER_APP_KEY и VITE_PUSHER_APP_CLUSTER) и передаём их экземпляру Echo. Это те же переменные окружения, которые мы ранее добавили в наш файл .env.

После создания экземпляра Echo можно скомпилировать активы, выполнив в терминале следующую команду

npm run dev

Хотя в браузере пока ничего не происходит, мы готовы написать код для прослушивания событий. Если все настроено правильно, Vite должен скомпилировать ваши ресурсы без каких-либо ошибок.

Прослушивание публичных каналов

Теперь, когда мы установили Pusher и настроили наше приложение, давайте рассмотрим, как использовать его для прослушивания публичных каналов.

В этом примере мы создадим простое представление Blade, содержащее кнопку. Когда кнопка будет нажата, это вызовет событие, которое будет транслироваться по публичному каналу. Мы будем слушать это событие в нашем JavaScript-коде и при его получении использовать функцию alert для вывода сообщения, переданного в событии.

Представление Blade может выглядеть следующим образом:

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">

<title>WebSockets Test</title>

@vite('resources/js/app.js')
</head>

<body>
<button id="submit-button" type="button">
Press Me!
</button>
</body>
</html>

Теперь нам нужно создать новый маршрут в файле routes/web.php. Когда мы нажмём на кнопку, мы отправим POST-запрос на этот маршрут, который запустит трансляцию:

use App\Http\Controllers\ButtonClickedController;
use Illuminate\Support\Facades\Route;

Route::post('button/clicked', ButtonClickedController::class);

Затем мы можем создать контроллер ButtonClickedController, выполнив в терминале следующую команду:

php artisan make:controller ButtonClickedController -i

Вы, наверное, заметили, что мы передаём в команду параметр -i. Это сделано для того, чтобы мы могли создать одноразовый контроллер, содержащий только метод __invoke, поскольку другие методы в нашем контроллере нам не нужны. Однако вы можете убрать флаг -i, если хотите создать полноценный контроллер.

Затем мы можем обновить наш контроллер таким образом, чтобы он отправлял событие при вызове и возвращал JSON-ответ:

app/Controllers/ButtonClickedController.php:

declare(strict_types=1);

namespace App\Http\Controllers;

use App\Events\ButtonClicked;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;

final class ButtonClickedController extends Controller
{
public function __invoke(Request $request): JsonResponse
{
ButtonClicked::dispatch(message: 'Hello world!');

return response()->json(['success' => true]);
}
}

Как видно из нашего контроллера, мы ссылаемся на событие ButtonClicked и передаём ему параметр message. Именно это событие мы будем транслировать через WebSockets. Мы можем создать это событие, выполнив в терминале следующую команду:

php artisan make:event ButtonClicked

Выполнение, приведённой выше команды, приведёт к созданию нового файла app/Events/ButtonClicked.php.

Я внёс некоторые изменения в генерируемый класс. Давайте посмотрим на класс, а затем разберём, что происходит:

app/Events/ButtonClicked.php:

declare(strict_types=1);

namespace App\Events;

use Illuminate\Broadcasting\Channel;
use Illuminate\Broadcasting\InteractsWithSockets;
use Illuminate\Contracts\Broadcasting\ShouldBroadcast;
use Illuminate\Foundation\Events\Dispatchable;
use Illuminate\Queue\SerializesModels;

final class ButtonClicked implements ShouldBroadcast
{
use Dispatchable;
use InteractsWithSockets;
use SerializesModels;

public function __construct(
private readonly string $message
) {
//
}

public function broadcastAs(): string
{
return 'button.clicked';
}

public function broadcastWith(): array
{
return [
'message' => $this->message,
];
}

public function broadcastOn(): array
{
return [
new Channel('public-channel'),
];
}
}

Во-первых, мы убедились, что класс реализует интерфейс Illuminate\Contracts\Broadcasting\ShouldBroadcast. Это необходимо для того, чтобы Laravel знал, что нужно транслировать событие через WebSocket-соединение. По умолчанию этот интерфейс приводит к тому, что событие обрабатывается и транслируется в очередь. Однако если вы предпочитаете транслировать событие синхронно (до возврата ответа на запрос), то вместо этого можно использовать интерфейс Illuminate\Contracts\Broadcasting\ShouldBroadcastNow.

Это заставит нас добавить метод broadcastOn к событию. Этот метод должен возвращать массив, содержащий канал(ы), на который мы хотим транслировать событие. В данном случае мы будем транслировать событие на канале public-channel.

Мы также добавим к нашему событию необязательный метод broadcastAs. Он будет возвращать имя события, которое мы можем транслировать. Назовём его button.clicked. Если мы не укажем метод, Laravel вернётся к имени класса события. В данном случае это будет App\Events\ButtonClicked. Важно помнить, что если вы укажете метод broadcastAs, то в Laravel Echo к началу имени события нужно будет добавить символ . в противном случае к имени события будет добавлено ожидаемое пространство имён (App\Events). В данном случае в нашем JavaScript мы хотим слушать .button.clicked вместо button.clicked.

Мы также можем указать метод broadcastWith. Он вернёт массив данных, которые мы хотим транслировать вместе с событием. В данном случае передаётся свойство message, которое мы передали в конструктор события и которое будет отображаться в окне оповещения JavaScript. Однако вы можете передать сюда любые данные, наиболее подходящие для вашей задачи. Например, если вы создаёте приложение для чата, то, возможно, захотите передавать сообщение, которое было отправлено, и некоторую другую метаинформацию о чате.

Это все, что требуется на бэкенде для отправки широковещательных сообщений. Теперь мы можем обновить фронтенд, чтобы он прослушивал трансляции и выводил окно оповещения.

Нам необходимо добавить слушатель событий, который при нажатии кнопки отправляет POST-запрос на маршрут /button/clicked. Мы знаем, что наше событие называется button-clicked и что оно транслируется на канале public-channel. Мы можем обновить файл resources/js/app.js, используя следующий код:

resources/js/app.js:

import './bootstrap';

// Создаём слушатель событий, отправляющий POST-запрос
// серверу, когда пользователь нажмёт на кнопку.
document.querySelector('#submit-button').addEventListener(
'click',
() => window.axios.post('/button/clicked')
);


// Подписываемся на публичный канал с именем "public-channel"
Echo.channel('public-channel')

// Прослушиваем событие с именем "button.clicked"
.listen('.button.clicked', (e) => {

// Отображение "сообщения" в окне оповещения
alert(e.message);
});

Теперь мы можем проверить, что это работает. Если нажать кнопку на странице, то в браузере должно появиться окно оповещения. Чтобы доказать, что это окно отображается с помощью WebSockets, можно открыть два разных браузера и перейти на одну и ту же страницу в каждом из них. Затем нажмите кнопку в одном браузере. В обоих браузерах должно появиться окно оповещения.

Прослушивание приватных каналов

До сих пор мы рассматривали отправку широковещательных сообщений по публичным каналам WebSocket. Однако бывают случаи, когда необходимо передавать сообщения по приватным каналам, чтобы их могли прослушивать только авторизованные пользователи. Например, если вы создаёте чат-приложение, то не хотите, чтобы ваши сообщения при отправке транслировались по публичному каналу; это позволит любому, кто знает имя канала, прослушать ваши сообщения. Вместо этого нужно отправлять сообщения по приватному каналу, чтобы их могли прослушать только те, кому они предназначены.

Рассмотрим, как можно использовать приватные каналы в нашем приложении.

По умолчанию Laravel Echo будет выполнять HTTP-запрос к конечной точке /broadcasting/auth в приложении, когда пользователь пытается подписаться на приватный канал. Эта конечная точка определена в классе app/Providers/BroadcastServiceProvider. Мы можем настроить эту конечную точку, переопределив метод broadcastingAuthEndpoint класса BroadcastServiceProvider и обновив конфигурацию Laravel Echo. Однако для целей данного руководства мы будем использовать маршрут по умолчанию.

Представим, что у нас есть чат-приложение, и мы хотим использовать приватные каналы. Чтобы определить логику авторизации для нашего приватного канала, потребуется использовать метод Broadcast::channel в файле routes/channels.php.

Этот метод принимает два аргумента: имя канала и обратный вызов, который будет выполняться при попытке пользователя подписаться на канал. Если пользователь авторизован для подписки на канал, то обратный вызов должен вернуть true или массив данных, которые будут отправлены клиенту (мы рассмотрим эту часть при обсуждении каналов присутствия позже). Если пользователь не авторизован для подписки на канал, обратный вызов должен возвращать false.

Будем считать, что модель Chat имеет метод isMember, который возвращает true, если пользователь является участником чата, и false, если не является.

Мы можем определить авторизацию приватного канала для нашего чат-приложения с помощью следующего кода в файле routes/channels.php:

use App\Models\Chat;
use App\Models\User;
use Illuminate\Support\Facades\Broadcast;

Broadcast::channel(
'chats.{chat}',
fn (User $user, Chat $chat): bool => $chat->isMember($user)
);

Как видно из приведённого примера, мы определили приватный канал chats.{chat}. Затем мы проверили, является ли пользователь участником чата, на который он пытается подписаться. Аналогично привязке модели маршрута в Laravel, определив {chat} в имени канала, мы можем ввести в обратный вызов модель Chat. Это позволит автоматически разрешить экземпляр модели Chat из параметра.

Теперь мы можем создать событие, которое будет транслироваться на приватный канал. Назовём это событие App\Events\MessageSent. Давайте рассмотрим класс события, а затем посмотрим, что было сделано:

declare(strict_types=1);

namespace App\Events;

use App\Models\Chat;
use App\Models\ChatMessage;
use Illuminate\Broadcasting\PrivateChannel;
use Illuminate\Broadcasting\InteractsWithSockets;
use Illuminate\Contracts\Broadcasting\ShouldBroadcast;
use Illuminate\Foundation\Events\Dispatchable;
use Illuminate\Queue\SerializesModels;

final class MessageSent implements ShouldBroadcast
{
use Dispatchable;
use InteractsWithSockets;
use SerializesModels;

public function __construct(
private readonly Chat $chat,
private readonly ChatMessage $chatMessage
) {
//
}

public function broadcastAs(): string
{
return 'message.sent';
}

public function broadcastWith(): array
{
return [
'message' => $this->chatMessage->message,
'sentBy' => [
'id' => $this->chatMessage->sentBy->id,
'name' => $this->chatMessage->sentBy->name,
]
];
}

public function broadcastOn(): array
{
return [
new PrivateChannel('chats.'.$this->chat->id),
];
}
}

Как мы видим, метод broadcastOn возвращает массив, содержащий объект PrivateChannel, а не Channel. Если предположить, что модель Chat имеет идентификатор 1, то событие будет транслироваться на приватный канал chats.1.

Вы также могли заметить, что конструктор включает модель Chat, чтобы мы могли получить к ней доступ. Для диспетчеризации такого события наш код может выглядеть следующим образом:

use App\Models\Chat;
use App\Events\MessageSent;

MessageSent::dispatch(chat: $chat, chatMessage: $chatMessage);

Теперь напишем пример JavaScript-кода, который можно использовать для подписки на приватный канал. Будем считать, что функция displayChatMessage отвечает за отображение сообщения чата в браузере. Предположим также, что функция getChatId возвращает идентификатор чата, который пользователь просматривает в данный момент.

const chatId = getChatId();

Echo.private(`chats.${chatId}`)
.listen('.message.sent', (e) => {
displayChatMessage(e.message, e.sentBy);
});

Как видите, это очень похоже на подписку на публичный канал. Единственное отличие заключается в том, что вместо метода канала мы используем метод private. Laravel Echo обрабатывает логику авторизации за нас, так что нам не нужно беспокоиться о том, как это делается.

Прослушивание каналов присутствия

Каналы присутствия являются приватными, но они позволяют иметь некоторое представление о том, кто подписан на данный канал. Такую возможность можно использовать в чат-приложении, чтобы знать, какие пользователи присутствуют в чате. Они предоставляют нам доступ к событиям, которые дают возможность видеть, когда кто-то присоединяется к каналу или покидает его.

Как и в случае с приватными каналами, при попытке присоединиться к каналу необходимо авторизовать пользователя. Вместо того чтобы возвращать в методе просто true или false, мы можем возвращать некоторые данные в виде массива. Например, возьмём наш предыдущий пример с приватными каналами для чата. При авторизации пользователя для канала chats.{chat} мы могли бы возвращать массив, содержащий идентификатор и имя пользователя. Эти данные затем транслировались бы другим пользователям, уже подписанным на этот канал. Это может быть использовано для отображения информации, например, Джон Смит присоединился к чату или Джон Смит покинул чат, или даже для выделения в списке пользователей, находящихся в данный момент в сети.

Для начала работы с каналом присутствия необходимо определить логику авторизации для канала. Мы сделаем это в файле routes/channels.php:

use App\Models\Chat;
use App\Models\User;
use Illuminate\Support\Facades\Broadcast;

Broadcast::channel('chats.{chat}', function (User $user, Chat $chat): bool {
return $chat->isMember($user)
? ['id' => $user->id, 'name' => $user->name]
: false;
});

Теперь, если бы мы захотели транслировать событие на этот канал, это было бы очень похоже на трансляцию на приватный канал. Единственное отличие заключается в том, что в массив метода broadcastOn нам нужно вернуть объект PresenceChannel, а не PrivateChannel:

use Illuminate\Broadcasting\PresenceChannel;

public function broadcastOn(): array
{
return [
new PresenceChannel('chats.'.$this->chat->id),
];
}

Теперь мы можем написать JavaScript, обрабатывающий логику канала присутствия.

Будем считать, что функция getChatId возвращает id чата, который пользователь просматривает в данный момент. Будем также считать, что у нас есть ещё четыре функции:

Чтобы присоединиться к каналу присутствия, мы можем использовать метод join в Laravel Echo:

const chatId = getChatId();

Echo.join(`chat.${chatId}`)
.here((users) => {
// Выполняется при первом присоединении к каналу.
highlightActiveUsers(users);
})
.joining((user) => {
// Выполняется, когда к каналу присоединяются другие пользователи.
displayUserJoinedMessage(user);
})
.leaving((user) => {
// Выполняется, когда пользователи покидают канал.
displayUserLeftMessage(user);
})
.listen('.message.sent', (e) => {
// Выполняется при отправке сообщения на канал.
displayChatMessage(e.message, e.sentBy);
})
.error((error) => {
// Выполняется, если возникла проблема с присоединением к каналу.
console.error(error);
});

Вы, наверное, заметили, что здесь используется пять методов. Давайте рассмотрим, что делает каждый из них:

Как видите, каналы присутствия могут помочь обеспечить более интерактивный опыт для пользователя без излишней сложности кода.

Дальнейшее развитие

Теперь, когда мы рассмотрели три основных типа каналов и способы их использования для передачи данных клиенту, давайте рассмотрим другие удобные функции, связанные с WebSockets, которые мы можем использовать в наших Laravel-приложениях.

Использование классов широковещательных каналов

По мере роста проекта вы можете обнаружить, что файл routes/channels.php становится довольно большим, особенно с логикой авторизации для приватных каналов. Это иногда затрудняет его чтение и сопровождение. Чтобы сохранить этот файл в чистоте и улучшить его сопровождаемость, мы можем воспользоваться возможностью, которую предоставляет Laravel, под названием классы каналов.

Классы каналов — это PHP-классы, которые позволяют нам инкапсулировать логику авторизации для приватных каналов внутри класса. Этот класс может быть использован в файле routes/channels.php для определения логики авторизации для канала.

Для примера возьмём следующее определение приватного канала из файла routes/channels.php:

use App\Models\Chat;
use App\Models\User;
use Illuminate\Support\Facades\Broadcast;

Broadcast::channel(
'chats.{chat}',
fn (User $user, Chat $chat): bool => $chat->isMember($user)
);

Мы можем создать новый класс канала, выполнив следующую команду:

php artisan make:channel ChatChannel

В результате будет создан класс app\Broadcasting\ChatChannel с методом join. В этот метод мы можем добавить нашу логику авторизации:

declare(strict_types=1);
namespace App\Broadcasting;
use App\Models\Chat;
use App\Models\User;
final class ChatChannel
{
public function join(User $user, Chat $chat): bool
{
return $chat->isMember($user);
}
}

В противном случае, если мы хотим возвращать массив данных (например, для использования в канале присутствия), метод join может выглядеть следующим образом:

public function join(User $user, Chat $chat): bool
{
return $chat->isMember($user)
? ['id' => $user->id, 'name' => $user->name]
: false;
}

Затем мы можем обновить наш маршрут канала в файле routes/channel.php, чтобы он использовал этот новый класс канала:

use App\Broadcasting\ChatChannel;
use Illuminate\Support\Facades\Broadcast;

Broadcast::channel('chats.{chat}', ChatChannel::class);

Трансляция другим пользователям

Бывают случаи, когда требуется отправить сообщение только другим пользователям, но не себе.

Например, представим, что вы используете чат-приложение и отправляете сообщение. В JavaScript приложении можно мгновенно отобразить сообщение на экране сразу после его отправки. Возможно также, что Laravel Echo настроен на прослушивание всех новых отправленных сообщений. Получив одно из таких событий, Laravel Echo отобразит сообщение на странице. Это прекрасно подходит для отображения сообщений в браузере получателя. Однако это приведёт к тому, что сообщение будет дважды отображаться в браузере отправителя (один раз во время отправки и второй раз во время получения события WebSocket).

Чтобы предотвратить подобные сценарии, можно использовать метод toOthers при диспетчеризации транслируемого события. Например, если мы хотим транслировать событие MessageSent другим пользователям в чате, мы можем сделать следующее:

use App\Events\MessageSent;

broadcast(
new MessageSent(chat: $chat, message: $chatMessage)
)->toOthers();

Вещание по нескольким каналам на каждое событие

До сих пор во всех приведённых примерах мы транслировали события только на один канал. Однако могут возникнуть ситуации, когда необходимо транслировать событие одновременно на несколько каналов. Допустим, в вашем приложении для каждого пользователя настроен свой личный канал, и вы хотите отправить уведомление сразу нескольким пользователям.

Для этого в методе broadcastOn события можно вернуть массив каналов. Например, допустим, у нас есть три пользователя, у которых включён свой приватный канал уведомлений (каналы называются notifications.1, notifications.2 и notifications.3). Мы можем отправить трансляцию каждому из пользователей, вернув массив классов PrivateChannel из метода broadcastOn:

app/Events/NewNotification.php:

declare(strict_types=1);

namespace App\Events;

use App\Models\User;
use Illuminate\Support\Collection;
use Illuminate\Broadcasting\PrivateChannel;
use Illuminate\Broadcasting\InteractsWithSockets;
use Illuminate\Contracts\Broadcasting\ShouldBroadcast;
use Illuminate\Foundation\Events\Dispatchable;
use Illuminate\Queue\SerializesModels;

final class NewNotification implements ShouldBroadcast
{
use Dispatchable;
use InteractsWithSockets;
use SerializesModels;

/**
* @param Collection<User> $users
*/

public function __construct(
private readonly Collection $users,
) {
//
}

// ...

public function broadcastOn(): array
{
return $this->users->map(function (User $user): PrivateChannel {
return new PrivateChannel('notifications.'.$user->id);
})->all();
}
}

В приведённом примере мы передаём конструктору события Collection моделей пользователей. В методе broadcastOn мы перебираем Collection и возвращаем массив экземпляров PrivateChannel (по одному экземпляру PrivateChannel на пользователя).

Шёпот

Laravel Echo позволяет использовать клиентские события. Эти события не передаются на сервер вашего приложения и могут быть очень полезны для обеспечения функциональности, например, индикаторов набора текста в приложении для чата.

Представим, что у нас есть чат-приложение, и мы хотим посылать клиентское событие другим участникам чата, когда кто-то набирает текст. Будем считать, что существует метод getUserName, который возвращает имя вошедшего в систему пользователя. Мы также будем использовать очень наивный и базовый подход для определения того, набирает ли пользователь текст. В реальных приложениях, возможно, потребуется добавить дебаунсинг, прослушивать пользователей, вставляющих текст в текстовое поле, и т.д. Однако для целей данного руководства мы просто будем посылать клиентское событие всякий раз, когда пользователь вводит текст в поле сообщения.

Для включения клиентских событий в JavaScript можно использовать методы whisper и listenForWhisper:

// Определите экземпляр канала.
let channel = Echo.private('chat');

// Добавляем обработку при обнаружении клиентского события. Это позволит
// выводить в консоль сообщение "John Smith is typing...".
channel.listenForWhisper('typing', (e) => {
console.log(e.name + ' is typing...');
})

// Добавляем слушатель событий, вызывающий клиентское событие "typing"
// событие, когда пользователь набирает текст в поле сообщения.
document.querySelector('#message-box')
.addEventListener('keydown', function (e) {
channel.whisper('typing', {
name: getUserName(),
});
});

Метод whisper будет вызываться всякий раз, когда пользователь набирает текст, и передавать данные другим пользователям, прослушивающим канал.

В браузере принимающего пользователя будет вызван метод listenForWhisper, которому будут переданы данные, отправленные методом whisper.

Важно отметить, что если вы используете Pusher (как показано в данном руководстве), то вам необходимо включить опцию Client Events в App Settings вашего приложения Pusher Channels.

Заключение

Эта статья должна была дать вам некоторое представление, что такое WebSockets, где вы можете их использовать и как настроить их в Laravel с помощью Pusher. Теперь вы должны уметь использовать WebSockets в приложениях Laravel для добавления функциональности реального времени с помощью публичных каналов, частных каналов и каналов присутствия. Вы также должны уметь использовать такие концепции, как классы каналов и клиентские события, для построения надёжных WebSockets-интеграций.

Дополнительные материалы

Предыдущая Статья

Глубокое погружение в Laravel Folio

Следующая Статья

Понимание module.exports и exports в Node.js