Во многих современных веб-приложениях WebSocket используется для реализации пользовательских интерфейсов, обновляемых в режиме реального времени. При обновлении данных на сервере обычно отправляется сообщение по WebSocket-соединению, которое обрабатывается клиентом. WebSocket предоставляет более эффективную альтернативу постоянному опросу сервера вашего приложения на предмет изменений данных, которые должны быть отражены в вашем пользовательском интерфейсе.
Например, представьте, что ваше приложение может экспортировать данные пользователя в CSV-файл и отправлять его по электронной почте. Однако создание CSV-файла занимает несколько минут, поэтому вы выбираете создание и отправку CSV-файла в рамках задания, поставленного в очередь . После создания CSV-файла и его отправки пользователю мы можем использовать трансляцию событий для отправки App\Events\UserDataExported события, которое получает JavaScript-код нашего приложения. После получения события мы можем отобразить пользователю сообщение о том, что CSV-файл был отправлен ему по электронной почте, без необходимости обновления страницы.
Чтобы помочь вам в создании подобных функций, Darklyy упрощает «трансляцию» серверных событий Darklyy через соединение WebSocket. Трансляция событий Darklyy позволяет использовать одни и те же имена событий и данные между серверным приложением Darklyy и клиентским JavaScript-приложением.
Основные принципы трансляции просты: клиенты подключаются к именованным каналам на фронтенде, а ваше приложение Darklyy транслирует события в эти каналы на бэкенде. Эти события могут содержать любые дополнительные данные, которые вы хотите сделать доступными на фронтенде.
Channel приватные PrivateChannel и каналы присутсвия PresenceChannel.Channel, PrivateChannel, PresenceChannel- usr
- modules
- {module-name}
- Boardcasts
- Channels
- ...
- Routes.php
- usr
- app
- Boardcasts
- Channels
- ...
- Routes.php
Скоро напишу
Прежде чем углубляться в каждый компонент трансляции событий, давайте рассмотрим его на примере интернет-магазина.
Предположим, что в нашем приложении есть страница, позволяющая пользователям просматривать статус доставки своих заказов. Также предположим, что событие OrderShipmentStatusUpdated срабатывает при обработке обновления статуса доставки приложением:
use App\Events\OrderShipmentStatusUpdated;
OrderShipmentStatusUpdated::dispatch($order);
ShouldBroadcastМы не хотим, чтобы пользователь обновлял страницу для просмотра статуса, когда он просматривает один из своих заказов. Вместо этого мы хотим транслировать обновления в приложение по мере их создания. Поэтому нам нужно отметить событие OrderShipmentStatusUpdatedс c помощью ShouldBroadcast интерфейса. Это даст команду Darklyy транслировать событие при его возникновении:
<?php
namespace App\Events;
use App\Models\Order;
use Boot\System\Broadcasting\Channel;
use Boot\System\Broadcasting\InteractsWithSockets;
use Boot\System\Broadcasting\PresenceChannel;
use Boot\Contracts\Broadcasting\ShouldBroadcast;
class OrderShipmentStatusUpdated implements ShouldBroadcast
{
/**
* The order instance.
*
* @var \App\Models\Order
*/
public $order;
}
Интерфейс ShouldBroadcast требует, чтобы наше событие определяло broadcastOn метод. Этот метод отвечает за возврат каналов, по которым событие должно транслироваться. Мы хотим, чтобы только создатель заказа мог просматривать обновления статуса, поэтому мы будем транслировать событие по приватному каналу, привязанному к заказу:
use Boot\System\Broadcasting\Channel;
use Boot\System\Broadcasting\PrivateChannel;
/**
* Получение канала, на котором будет транслироваться событие.
*/
public function broadcastOn(): Channel
{
return new PrivateChannel('orders.'.$this->order->id);
}
Если вы хотите, чтобы событие транслировалось по нескольким каналам, вы можете array вместо этого вернуть:
use Boot\System\Broadcasting\PrivateChannel;
/**
* Получение каналов, на которых будет транслироваться событие.
*
* @return array<int, \Boot\System\Broadcasting\Channel>
*/
public function broadcastOn(): array
{
return [
new PrivateChannel('orders.'.$this->order->id),
// ...
];
}
Помните, что пользователи должны быть авторизованы для прослушивания приватных каналов. Мы можем определить правила авторизации каналов в файле маршрутов каналов модуля или общем файле маршрутов приложения App/Boardcasts/Routes.php. В этом примере нам нужно убедиться, что любой пользователь, пытающийся прослушивать приватный orders.1 канал, действительно является создателем заказа:
use App\Models\Order;
use App\Models\User;
Broadcast::channel('orders.{orderId}', function (User $user, int $orderId) {
return $user->id === Order::findOrNew($orderId)->user_id;
});
Метод channel принимает два аргумента: имя канала и обратный вызов, который возвращает значение true или false указывает, авторизован ли пользователь для прослушивания канала.
Все обратные вызовы авторизации получают модель текущего аутентифицированного пользователя в качестве первого аргумента, а любые дополнительные параметры-шаблоны — в качестве последующих аргументов. В этом примере мы используем {orderId}, чтобы указать, что часть «ID» в имени канала является шаблоном.
Теперь остаётся только прослушивать событие в нашем JavaScript-приложении. Для этого можно использовать Echo:
window.Echo.private('orders.1').listen('.App\\Events\\OrderShipmentStatusUpdated', (e) => {
console.log(e.order);
});
Чтобы сообщить Darklyy о необходимости трансляции события, необходимо реализовать соответствующий Boot\Contracts\Broadcasting\ShouldBroadcast интерфейс в классе события.
Интерфейс ShouldBroadcast требует реализации одного метода: broadcastOn. Метод broadcastOn должен возвращать канал или массив каналов, на которых должно транслироваться событие. Каналы должны быть экземплярами Channel, PrivateChannel, или PresenceChannel. Экземпляры Channel представляют публичные каналы, на которые может подписаться любой пользователь, а экземпляры PrivateChannels и PresenceChannels представляют частные каналы, требующие авторизации канала:
<?php
namespace App\Events;
use App\Models\User;
use Boot\System\Broadcasting\Channel;
use Boot\System\Broadcasting\InteractsWithSockets;
use Boot\System\Broadcasting\PresenceChannel;
use Boot\System\Broadcasting\PrivateChannel;
use Boot\Contracts\Broadcasting\ShouldBroadcast;
use Illuminate\Queue\SerializesModels;
class ServerCreated implements ShouldBroadcast
{
use SerializesModels;
/**
* Создание нового экземпляра события
*/
public function __construct(
public User $user,
) {}
/**
* Получение каналов, на которых будет транслироваться событие.
*
* @return array<int, \Boot\System\Broadcasting\Channel>
*/
public function broadcastOn(): array
{
return [
new PrivateChannel('user.'.$this->user->id),
];
}
}
После реализации ShouldBroadcast интерфейса вам останется только инициировать событие обычным способом. После этого поставленное в очередь задание автоматически передаст его в эфир, используя указанный вами драйвер вещания.
По умолчанию Darklyy транслирует событие, используя имя класса события. Однако вы можете изменить имя трансляции, определив broadcastAs метод для события:
/**
* Имя события трансляции
*/
public function broadcastAs(): string
{
return 'server.created';
}
Если вы настраиваете имя трансляции с помощью этого метода broadcastAs, обязательно зарегистрируйте слушателя, добавив к нему начальный символ . . Это предупредит Echo не добавлять пространство имён приложения к событию:
.listen('.server.created', function (e) {
// ...
});
При трансляции события все его public свойства автоматически сериализуются и транслируются в качестве полезной нагрузки события, что позволяет вам получить доступ к любым его открытым данным из вашего JavaScript-приложения. Например, если у вашего события есть одно открытое $user свойство, содержащее модель Eloquent, полезная нагрузка трансляции события будет следующей:
{
"user": {
"id": 1,
"name": "Patrick Stewart"
...
}
}
Однако, если вам требуется более точный контроль над передаваемыми данными, вы можете добавить метод broadcastWith к событию. Этот метод должен возвращать массив данных, которые вы хотите передать в качестве передаваемых данных события:
/**
* Получение данных для трансляции
*
* @return array<string, mixed>
*/
public function broadcastWith(): array
{
return ['id' => $this->user->id];
}
По умолчанию каждое событие трансляции рассылки помещается в очередь по умолчанию для подключения к очереди по умолчанию, указанного в файле конфигурации queue.config.php. Вы можете настроить подключение к очереди и имя, используемые вещателем, определив свойства connection и queue класс событий:
/**
* Имя подключения к очереди, которое будет использоваться при трансляции события.
*
* @var string
*/
public $connection = 'redis';
/**
* Имя очереди, в которую следует поместить задание по вещанию.
*
* @var string
*/
public $queue = 'default';
В качестве альтернативы вы можете настроить имя очереди, определив метод broadcastQueue для вашего события:
/**
* Имя очереди, в которую следует поместить задание по вещанию.
*/
public function broadcastQueue(): string
{
return 'default';
}
Если вы хотите транслировать свое событие, используя sync очередь вместо драйвера очереди по умолчанию, вы можете реализовать ShouldBroadcastNow интерфейс вместо ShouldBroadcast:
<?php
namespace App\Events;
use Boot\Contracts\Broadcasting\ShouldBroadcastNow;
class OrderShipmentStatusUpdated implements ShouldBroadcastNow
{
// ...
}
Иногда требуется транслировать событие только при выполнении заданного условия. Вы можете определить эти условия, добавив broadcastWhen метод в класс события:
/**
* Определение, следует ли транслировать это событие.
*/
public function broadcastWhen(): bool
{
return $this->order->value > 100;
}
Когда широковещательные события отправляются в рамках транзакций базы данных, они могут быть обработаны очередью до того, как транзакция будет зафиксирована. В этом случае любые обновления моделей или записей базы данных, внесённые вами во время транзакции, могут ещё не быть отражены в базе данных. Кроме того, любые модели или записи базы данных, созданные в рамках транзакции, могут отсутствовать в базе данных. Если ваше событие зависит от этих моделей, при обработке задания, транслирующего событие, могут возникнуть непредвиденные ошибки.
Если параметр конфигурации соединения вашей очереди after_commitу становлен на false, вы все равно можете указать, что определенное событие вещания должно быть отправлено после того, как все открытые транзакции базы данных будут зафиксированы, реализовав ShouldDispatchAfterCommit интерфейс в классе событий:
<?php
namespace App\Events;
use Boot\Contracts\Broadcasting\ShouldBroadcast;
use Boot\Contracts\Events\ShouldDispatchAfterCommit;
use Illuminate\Queue\SerializesModels;
class ServerCreated implements ShouldBroadcast, ShouldDispatchAfterCommit
{
use SerializesModels;
}
Приватные каналы требуют разрешения для текущего аутентифицированного пользователя на прослушивание. Это достигается путем отправки HTTP-запроса к вашему приложению Darklyy с указанием имени канала и разрешения приложению определить, может ли пользователь прослушивать этот канал. При использовании Echo HTTP-запрос на авторизацию подписки на приватные каналы будет выполнен автоматически.
При включении трансляции Darklyy автоматически регистрирует /broadcasting/auth маршрут для обработки запросов на авторизацию. /broadcasting/auth Маршрут автоматически помещается в web группу промежуточного ПО. Кроме того данный маршрут регистрируется в группе api где авторизация будет проходить не по сесси а через sanctum
Далее нам нужно определить логику, которая будет определять, может ли текущий аутентифицированный пользователь прослушивать заданный канал. Это делается в файле маршрутов каналов модуля или общем файле маршрутов приложения App/Boardcasts/Routes.php. В этом файле можно использовать Broadcast::channel метод для регистрации обратных вызовов авторизации канала:
use App\Models\User;
Broadcast::channel('orders.{orderId}', function (User $user, int $orderId) {
return $user->id === Order::findOrNew($orderId)->user_id;
});
Метод channel принимает два аргумента: имя канала и обратный вызов, который возвращает значение true или false указывает, авторизован ли пользователь для прослушивания канала.
Все обратные вызовы авторизации получают имя текущего аутентифицированного пользователя в качестве первого аргумента, а любые дополнительные параметры-шаблоны — в качестве последующих аргументов. В этом примере мы используем {orderId}, чтобы указать, что часть «ID» в имени канала является шаблоном.
Вы можете просмотреть список обратных вызовов авторизации широковещательной передачи вашего приложения с помощью команды darksander channel:list:
php darksander channel:list
Как и HTTP-маршруты, канальные маршруты также могут использовать неявную и явную привязку к модели маршрута . Например, вместо получения строкового или числового идентификатора заказа вы можете запросить фактический Order экземпляр модели:
use App\Models\Order;
use App\Models\User;
Broadcast::channel('orders.{order}', function (User $user, Order $order) {
return $user->id === $order->user_id;
});
В отличие от привязки модели HTTP-маршрута, привязка модели канала не поддерживает автоматическое неявное определение области действия привязки модели. Однако это редко вызывает проблемы, поскольку область действия большинства каналов можно определить на основе уникального первичного ключа одной модели.
Частные каналы и каналы вещания присутствия аутентифицируют текущего пользователя с помощью стандартного сторожа аутентификации вашего приложения. Если пользователь не аутентифицирован, авторизация канала автоматически отклоняется, и обратный вызов авторизации не выполняется. Однако вы можете назначить несколько пользовательских сторожей, которые при необходимости будут аутентифицировать входящий запрос:
Broadcast::channel('channel', function () {
// ...
}, ['guards' => ['web', 'admin']]);
Вместо использования замыканий для авторизации каналов можно использовать классы каналов.
Регистрация каналов через класс в файле маршрутов каналов модуля или общем файле маршрутов приложения App/Boardcasts/Routes.php выгдлядит так:
use App\Broadcasting\OrderChannel;
Broadcast::channel('orders.{order}', OrderChannel::class);
Наконец, вы можете поместить логику авторизации для вашего канала в join метод класса канала. Этот join метод будет содержать ту же логику, которую вы обычно помещаете в замыкание авторизации канала. Вы также можете воспользоваться привязкой к модели канала:
<?php
namespace App\Broadcasting;
use App\Models\Order;
use App\Models\User;
class OrderChannel
{
public function __construct() {}
public function join(User $user, Order $order): array|bool
{
return $user->id === $order->user_id;
}
}
После определения события и его маркировки с помощью ShouldBroadcast интерфейса вам остаётся только инициировать событие, используя метод отправки события. Диспетчер событий заметит, что событие отмечено с помощью ShouldBroadcast интерфейса, и поставит его в очередь на трансляцию:
use App\Events\OrderShipmentStatusUpdated;
OrderShipmentStatusUpdated::dispatch($order);
При разработке приложения, использующего трансляцию событий, иногда может потребоваться транслировать событие всем подписчикам заданного канала, за исключением текущего пользователя. Это можно сделать с помощью функции broadcast и вспомогательного метода toOthers:
use App\Events\OrderShipmentStatusUpdated;
broadcast(new OrderShipmentStatusUpdated($update))->toOthers();
Чтобы лучше понять, когда может понадобиться использовать этот метод toOthers, представим себе приложение со списком задач, в котором пользователь может создать новую задачу, введя её название. Для создания задачи ваше приложение может отправить запрос по URL-адресу /task, который сообщит о создании задачи и вернёт её JSON-представление. Получив ответ от конечной точки, ваше JavaScript-приложение может напрямую добавить новую задачу в свой список задач, например:
axios.post('/task', task)
.then((response) => {
this.tasks.push(response.data);
});
Однако помните, что мы также транслируем создание задачи. Если ваше JavaScript-приложение также прослушивает это событие для добавления задач в список, в вашем списке будут дублирующиеся задачи: одна из конечной точки и одна из транслируемого объекта. Решить эту проблему можно, используя метод, toOthers чтобы указать транслирующему объекту не транслировать событие текущему пользователю.
Для корректной работы данного метда, вам потребуется вручную настроить JavaScript-приложение для отправки заголовка X-Socket-ID со всеми исходящими запросами. Идентификатор сокета можно получить с помощью Echo.socketId метода:
var socketId = Echo.socketId();
Иногда может потребоваться транслировать простое событие во фронтенд приложения, не создавая отдельный класс событий. Для этого Broadcast фасад позволяет транслировать «анонимные события»:
Broadcast::on('orders.'.$order->id)->send();
В приведенном выше примере будет транслироваться следующее событие:
{
"event": "AnonymousEvent",
"data": "[]",
"channel": "orders.1"
}
Используя методы as и with, вы можете настроить имя и данные события:
Broadcast::on('orders.'.$order->id)
->as('OrderPlaced')
->with($order)
->send();
В приведенном выше примере будет транслироваться событие следующего вида:
{
"event": "OrderPlaced",
"data": "{ id: 1, total: 100 }",
"channel": "orders.1"
}
Если вы хотите транслировать анонимное событие на частном или публичном канале, вы можете использовать методы private и presence:
Broadcast::private('orders.'.$order->id)->send();
Broadcast::presence('channels.'.$channel->id)->send();
Трансляция анонимного события с помощью отправляет событие в очередьsend вашего приложения для обработки. Однако, если вы хотите транслировать событие немедленно, вы можете использовать метод sendNow
Broadcast::on('orders.'.$order->id)->sendNow();
Чтобы транслировать событие всем подписчикам канала, за исключением текущего аутентифицированного пользователя, вы можете вызвать toOthers метод:
Broadcast::on('orders.'.$order->id)
->toOthers()
->send();