{{TOC}} С помощью инструмента Laravel Echo вы легко сможете использовать мощь WebSockets в своих Laravel-приложениях. Он упрощает самые необходимые и самые трудные аспекты построения сложных взаимодействий WebSockets. Echo состоит из двух частей: набора улучшений для системы вещания сообщений Laravel (Event broadcasting system), и нового пакета JavaScript. Бэкендовые компоненты Echo уже встроены в ядро Laravel, начиная с версии 5.3, их не надо импортировать (в этом их отличие от таких компонентов, как ((https://laravel.ru/docs/v5/billing Cashier))). Вы можете использовать эти бэкендовые улучшения с любым JavaScript-фронтендом, а не только с JavaScript-библиотекой Echo, и при этом всё равно получите значительные упрощение работы с WebSockets. Но с JavaScript-библиотекой Echo они работают ещё лучше. JavaScript-библиотеку ((https://www.npmjs.com/package/laravel-echo Echo)) можно импортировать через NPM, а затем импортировать в JavaScript вашего приложения. Это слой "сахара" поверх ((https://github.com/pusher/pusher-js Pusher JS)) (JavaScript SDK для ((https://pusher.com/ Pusher))), либо поверх ((http://socket.io/ Socket.io)) (многие используют этот JavaScript SDK поверх архитектуры Redis WebSockets). ==Когда мне пригодится Echo?== Перед тем, как пойти дальше, давайте рассмотрим, как вы можете использовать Echo, чтобы понять, нужно ли вам это. Вам будут полезны WebSockets, если вы хотите посылать сообщения вашим пользователям - будь то уведомления или даже обновления структуры данных страницы - пока пользователи находятся на одной странице. Да, вы можете реализовать это с помощью длинных опросов (long-polling), или какого-нибудь регулярного пинга JavaScript, но при этом вы рискуете очень быстро уронить свой сервер. WebSockets мощны, не перегружают ваши сервера, легко масштабируются, и почти мгновенны. Если вы хотите использовать WebSockets в Laravel-приложении, то Echo обеспечивает приятный, чистый синтаксис для простых функций, таких как общедоступные каналы, и сложных функций, таких как аутентификация, авторизация, частные каналы и каналы присутствия. .(alert) Важный нюанс перед началом работы: реализация WebSockets предоставляет три типа каналов: //public - общедоступные// - подписаться может каждый; //private - частные// - фронтенд должен аутентифицировать пользователя для бэкенда и убедиться, что у пользователя есть право на подписку на этот канал; //presence - каналы присутствия// - которые не позволяют отправку сообщений, а только уведомляют, присутствует ли пользователь на канале. ==Перед началом работы с Echo: настройка примера рассылки события в Laravel== Предположим, вы хотите создать систему чатов с множеством комнат. Амбициозно, правда? Тогда нам надо генерировать событие каждый раз, когда приходит новое сообщение в чат. .(alert) Для полноценного понимания этой статьи, вы должны быть знакомы с рассылкой событий Laravel. Сначала стоит прочесть моё краткое ((https://mattstauffer.co/blog/broadcasting-events-with-pusher-socket-in-laravel-5.1 введение в рассылку событий)). Итак, сначала создадим событие: %%(sh) php artisan make:event ChatMessageWasReceived %% Откройте этот класс (%%(t)app/Events/ChatMessageWasReceived.php%%) и пометьте его как реализующий интерфейс %%ShouldBroadcast%%. А теперь давайте просто сделаем его рассылку в канал с названием %%(t)"chat-room.1"%%. .(alert) В версии 5.3 у метода %%broadcastOn()%% новая структура, которая освобождает вас от необходимости определять частные каналы и каналы присутствия с помощью префиксов "private-" и "presence-". Теперь вы можете просто обернуть имя канала в простой объект %%PrivateChannel%% или %%PresenceChannel%%. Итак, чтобы сделать рассылку в общедоступный канал - %%return "chat-room.1";%%. Для рассылки в частный канал - %%return new PrivateChannel("chat-room.1");%%. А для рассылки в канал присутствия - %%return new PresenceChannel("chat-room.1");%%. Вероятно, вам понадобится создать модель и миграцию для %%ChatMessage%%, и создать в ней поля %%(t)user_id%% и %%(t)message%%. %%(sh) php artisan make:model ChatMessage --migration %% Вот пример миграции: %% ... class CreateChatMessagesTable extends Migration { public function up() { Schema::create('chat_messages', function (Blueprint $table) { $table->increments('id'); $table->string('message'); $table->integer('user_id')->unsigned(); $table->timestamps(); }); } public function down() { Schema::drop('chat_messages'); } } %% А теперь давайте обновим наше событие, чтобы внедрить пользователя и сообщение: %% ... class ChatMessageWasReceived extends Event implements ShouldBroadcast { use InteractsWithSockets, SerializesModels; public $chatMessage; public $user; public function __construct($chatMessage, $user) { $this->chatMessage = $chatMessage; $this->user = $user; } public function broadcastOn() { return [ "chat-room.1" ]; } } %% И сделаем наши поля заполняемыми в модели: %% ... class ChatMessage extends Model { public $fillable = ['user_id', 'message']; } %% Теперь создадим способ для вызова этого события. Для тестирования я часто создаю Artisan-команду для вызова своих событий. Давайте попробуем. %%(sh) php artisan make:command SendChatMessage %% Откройте этот файл %%(t)app/Console/Commands/SendChatMessage.php%%. Задайте для него описание, позволяющее вам передать в него сообщение, а затем добавьте в метод %%handle()%% вызов нашего события %%ChatMessageWasReceived%% с этим сообщением: %% ... class SendChatMessage extends Command { protected $signature = 'chat:message {message}'; protected $description = 'Send chat message.'; public function handle() { // Вызов события, пока просто выбирая первого пользователя $user = \App\User::first(); $message = \App\ChatMessage::create([ 'user_id' => $user->id, 'message' => $this->argument('message') ]); event(new \App\Events\ChatMessageWasReceived($message, $user)); } } %% Теперь откройте %%(t)app/Console/Kernel.php%% и добавьте имя класса этой команды в свойство %%$commands%%, чтобы зарегистрировать её как переменную Artisan-команду. %% ... class Kernel extends ConsoleKernel { protected $commands = [ Commands\SendChatMessage::class, ]; ... %% Почти готово! Наконец, вам надо зарегистрировать аккаунт в ((https://pusher.com/ Pusher)) (Echo также работает с Redis и Socket.io, но для примера мы используем Pusher). Создайте новое приложение в своём Pusher-аккаунте и скопируйте key, secret и App ID. Теперь задайте эти значения в своём файле %%(t).env%% в %%(t)PUSHER_KEY%%, %%(t)PUSHER_SECRET%% и %%(t)PUSHER_APP_ID%%. И ещё, пока вы здесь, задайте параметру %%(t)BROADCAST_DRIVER%% значение %%(t)pusher%%. И наконец, запросите библиотеку Pusher: %%(sh) composer require pusher/pusher-php-server:~2.0 %% Теперь вы можете посылать события в ваш Pusher-аккаунт выполняя такие команды: %%(sh) php artisan chat:message "Всем приветики" %% Если всё сработает правильно, вы сможете войти в свою отладочную консоль в Pusher, вызвать это событие и увидеть такое: {{Image /packages/proger/habravel/uploads/655-pusher-debug.png, height=150px}} == Echo == Теперь у вас есть простая система для отправки событий в Pusher. Посмотрим, что даёт нам Echo. === Установка JS-библиотеки Echo === Простейший способ установить JavaScript-библиотеку Echo в ваш проект - импортировать её с помощью NPM и Elixir. Давайте сначала импортируем их самих и Pusher JS: %%(sh) # Установка основнхых зависимостей Elixir npm install # Установка Pusher JS и Echo, и добавление в package.json npm install --save laravel-echo pusher-js %% Затем настроим %%(t)resouces/assets/js/app.js%% для импорта: %%(JS) import Echo from "laravel-echo" window.Echo = new Echo({ broadcaster: 'pusher', key: 'здесь-ваш-ключ-pusher' }); // @todo: настроить привязки Echo здесь %% Наконец, запустите %%(sh)gulp%% или %%(sh)gulp watch%% и не забудьте привязать файл вывода результатов к своему HTML-шаблону. .(alert) Если вы используете чистую установку Laravel, то выполните %%(sh)php artisan make:auth%% вместо того, чтобы писать весь HTML вручную. Для последующих функций всё равно потребуется аутентификация, поэтому сделаем её сейчас. Echo нужен доступ к вашим CSRF-токенам. Если вы используете начальную загрузку авторизации Laravel, то она сделает токены доступными для Echo через %%(t)Laravel.csrfToken%%. А если не используете, то можете сделать это сами, создав мета-тег %%(t)csrf-token%%: %%(html) ... ... ... %% Фантастика! Давайте изучим синтаксис. === Подписка на общедоступные каналы с помощью Echo === Давайте вернёмся к %%(t)resources/assets/js/app.js%% и послушаем общедоступный канал %%chat-room.1%%, в который мы вещаем наше событие, и запишем все сообщения, пришедшие в пользовательскую консоль: %% import EchoLibrary from "laravel-echo" window.Echo = new EchoLibrary({ broadcaster: 'pusher', key: 'здесь-ваш-ключ-pusher' }); Echo.channel('chat-room.1') .listen('ChatMessageWasReceived', (e) => { console.log(e.user, e.chatMessage); }); %% Мы сказали Echo: подпишись на общедоступный канал %%chat-room.1%%. Слушай событие %%ChatMessageWasReceived%% (обратите внимание, что Echo не требует указания полного пространства имён). И когда получишь это событие, передай его в эту анонимную функцию и выполни её. Теперь посмотрим в нашу консоль: {{Image /packages/proger/habravel/uploads/655-echo-public-console-log.png, height=40px}} Вжух! Всего несколько строк кода, и у нас есть полный доступ к JSON-представлениям нашего сообщения и нашего пользователя. Великолепно! Мы можем использовать эти данные не только для отправки сообщений пользователям, но и для изменения данных в системах хранения "в памяти" в вашем приложении (VueJS, React, или другой), позволяя каждому WebSocket сообщению изменять содержимое текущей страницы. Теперь давайте перейдём к частным каналам и каналам присутствия, для которых необходимо использование аутентификации и авторизации. == Подписка на частные каналы с помощью Echo == Давайте сделаем %%chat-room.1%% частным. Сначала нам надо добавить %%private-%% к названию канала. Отредактируйте метод %%broadcastsOn()%% нашего Laravel-события %%ChatMessageWasReceived%%, чтобы название канала было %%private-chat-room.1%%. Или, для упрощения, вы можете передать название канала в новый экземпляр %%PrivateChannel%%, который делает то же самое: %%return new PrivateChannel('chat-room.1');%%. Затем мы используем %%Echo.private()%% в %%(t)app.js%% вместо %%Echo.channel()%%. Всё остальное можно оставить без изменений. Но если вы попытаетесь запустить скрипт, то заметите, что он не работает, а в консоли увидите такую ошибку: {{Image /packages/proger/habravel/uploads/655-echo-auth-not-found.png, height=80px}} Это намёк на следующую большую функцию, которую обрабатывает Echo: аутентификация и авторизация. === Основы аутентификации и авторизации Echo === В системе авторизации две части. Первая - когда вы открываете своё приложение впервые, Echo хочет выполнить запрос POST к вашему маршруту %%(t)/broadcasting/auth%%. Поскольку мы уже настроили инструменты Echo на стороне Laravel, этот маршрут свяжет ID вашего сокета Pusher и ID сессии Laravel. Теперь Laravel и Pusher знают, как проверить, что любое соединение сокета Pusher подключено к соответствующей сессии Laravel. Вторая часть аутентификации и авторизации Echo - когда вы обращаетесь к защищённому ресурсу (частный канал или канал присутствия), Echo выполнит "ping" по маршруту %%(t)/broadcasting/auth%% для проверки наличия у вас доступа к этому каналу. Поскольку ID вашего сокета связано с вашей сессией Laravel, мы можем написать простые и понятные ((https://ru.wikipedia.org/wiki/ACL ACL))-правила для этого маршрута. Давайте приступим. Сначала откройте %%(t)config/app.php%% и найдите %%App\Providers\BroadcastServiceProvider::class,%% и раскомментируйте это. Теперь откройте этот файл (%%(t)app/Providers/BroadcastServiceProvider.php%%). Там должно быть что-то такое: %% ... class BroadcastServiceProvider extends ServiceProvider { public function boot() { Broadcast::routes(); /* * Аутентификация частного канала пользователя... */ Broadcast::channel('App.User.*', function ($user, $userId) { return (int) $user->id === (int) $userId; }); } %% Здесь два важных момента. Первый - %%Broadcast::routes()%% регистрирует маршруты вещания, которые Echo использует для аутентификации и авторизации. Второй - вызов %%Broadcast::channel()%% даёт вам возможность задать права доступа к каналу или группе каналов (для указания нескольких каналов используется символ %%*%%). В Laravel канал по умолчанию связан с конкретным пользователем, чтобы показать, как выглядит ограничение доступа для одного, авторизованного в данный момент пользователя. ==== Создание прав доступа для нашего частного канала ==== Итак, у нас есть частный канал %%chat-room.1%%. Предполагается, что у нас будет несколько комнат чата (%%chat-room.2%%, и т.д.), поэтому давайте зададим права для всех комнат чата: %% Broadcast::channel('chat-room.*', function ($user, $chatroomId) { // вернуть, авторизован ли текущий пользователь на вход в эту комнату чата }); %% Как видите, первое значение, передаваемое в замыкание, - текущий пользователь, и если есть символы для подстановки вместо символа %%*%%, то они будут переданы в качестве дополнительных параметров. В рамках этой статьи мы просто сделаем заглушку авторизации, чтобы не создавать модель и миграцию для комнат чата, не добавлять связь "многие ко многим" с пользователем, и не проверять в этом замыкании, подключен ли текущий пользователь к этой комнате чата (что-то такое: %%if ($user->chatrooms->contains($chatroomId))%%). Сейчас давайте просто сделаем так: %% Broadcast::channel('chat-room.*', function ($user, $chatroomId) { if (true) { // Заменить настоящими правилами ACL return true; } }); %% Давайте проверим и посмотрим, что получилось. .(alert) Есть проблемы? Не забудьте, вам надо настроить ваш %%(t)app.js%% на использование %%echo.private()%% вместо %%echo.channel()%%; вам надо изменить ваше событие для вещания в частный канал %%chat-room-1%% вместо общедоступного канала; и вам надо отредактировать %%BroadcastServiceProvider%%. И ещё вам надо войти в ваше приложение. И вам надо перезапустить %%(sh)gulp%%, если вы не используете %%(sh)gulp watch%%. Вы должны увидеть пустую консоль, затем вы можете вызвать нашу Artisan-команду, и увидите своего пользователя и сообщение - как и раньше, но теперь доступ есть только у аутентифицированных и авторизованных пользователей! Если вместо этого вы видите следующее сообщение, это хорошо! Это значит, что всё работает, и ваша система решила, что вы //не// авторизованы на этом канале. Перепроверьте весь свой код, но помните, что всё работает - просто вы не авторизованы. {{Image /packages/proger/habravel/uploads/655-echo-403.png, height=50px}} Убедитесь, что вошли в систему, и попробуйте снова. == Подписка на каналы присутствия с помощью Echo == Итак, теперь мы можем решать в "бэкенде", у каких пользователей есть доступ к определённым комнатам чата. Когда пользователь посылает сообщение в комнату чата (обычно посылая AJAX-запрос на сервер, но в нашем примере - с помощью Artisan-команды), произойдёт событие %%ChatMessageWasReceived%%, которое будет разослано отдельно каждому пользователю через WebSockets. Что дальше? Предположим, мы хотим настроить индикатор для комнаты чата, показывающий, кто в ней; и может быть, мы хотим подавать звуковой сигнал при входе и выходе пользователей. Для этого есть инструмент, и он называется канал присутствия. Для этого нам потребуется две вещи: определение разрешений нового %%Broadcast::channel()%% и новый канал с префиксом %%presence-%% (который мы создадим, вернув экземпляр %%PresenceChannel%% из метода %%broadcastOn%% события). Это интересно, потому что определения авторизации каналов не требуют префиксов %%private-%% и %%presence-%%, ссылаться на %%private-chat-room.1%% и на %%presence-chat-room.1%% в вызовах %%Broadcast::channel()%% будем одинаково: %%chat-room.*%%. //Это на самом деле удобно//, пока вы используете для них одинаковые правила авторизации. Но это может привести к путанице, поэтому сейчас назовём каналы немного по-другому. Давайте используем %%presence-chat-room-presence.1%%, который будем авторизовывать как %%chat-room-presence.1%%. Итак, поскольку мы говорим только о присутствии, нам не надо привязывать этот канал к событию. Вместо этого мы просто дадим %%app.js%% указание, подключить нас к каналу: %% Echo.join('chat-room-presence.1') .here(function (members) { // запускается, когда вы заходите, и когда кто-либо другой заходит или выходит console.table(members); }); %% Мы "заходим" на канал присутствия, а затем добавляем обратный вызов, который выполнится один раз при загрузке этой страницы, а затем будет выполняться каждый раз, когда другой участник заходит или выходит из этого канала присутствия. Вдобавок к %%here%%, который вызывается по всем трём событиям, вы можете добавить слушателя для %%then%% (вызывается при входе пользователя), %%joining%% (вызывается при входе на канал других пользователей), и %%leaving%% (вызывается при выходе с канала других пользователей). %% Echo.join('chat-room-presence.1') .then(function (members) { // запускается, когда вы заходите console.table(members); }) .joining(function (joiningMember, members) { // запускается, когда входит другой пользователь console.table(joiningMember); }) .leaving(function (leavingMember, members) { // запускается, когда выходит другой пользователь console.table(leavingMember); }); %% Теперь давайте настроим авторизационные права для этого канала в %%BroadcastServiceProvider%%: %% Broadcast::channel('chat-room-presence.*', function ($user, $roomId) { if (true) { // Заменить настоящей авторизацией return [ 'id' => $user->id, 'name' => $user->name ]; } }); %% Как видите, канал присутствия не просто возвращает %%true%%, если пользователь аутентифицирован; он должен вернуть массив данных пользователя, которые вы хотите сделать доступными, например, для использования в боковой панели "пользователи онлайн". .(alert) Вы можете удивиться тому, что можно использовать одинаковое определение %%Broadcast::channel()%% и для частных каналов, и для каналов присутствия с похожими именами (%%private-chat-room.*%% и %%presence-chat-room.*%%), потому что замыкания частных каналов должны возвращать логический тип, а замыкания каналов присутствия - массив. Однако, возврат массива тоже "логичен", и воспринимается как "да", этот пользователь имеет доступ к каналу. Если всё соединилось правильно, то вы сможете открыть это приложение в двух разных браузерах и увидеть обновляемый список участников, выводимый в консоль при каждом входе или выходе другого пользователя: {{Image /packages/proger/habravel/uploads/655-echo-members-in-and-out-table.png, height=160px}} Теперь вы можете представить, как звонить в колокольчик каждый раз при входе и выходе пользователя, вы можете обновлять свой список участников "в памяти" JavaScript и привязать это к списку "пользователи онлайн" на странице, и многое другое. == Исключение текущего пользователя == Есть ещё одна вещь, которую делает Echo: что делать, если не надо отправлять уведомления текущему пользователю? Возможно, вы хотите показывать временное всплывающее сообщение вверху экрана, когда в чат приходит новое сообщение. Но вы же не хотите показывать его пользователю, который его //отправил//, верно? Чтобы исключить текущего пользователя из получателей сообщения, используйте для вызова события вместо метода %%event()%% вспомогательный метод %%broadcast()%% вместе с вызовом %%toOthers()%%: %% broadcast(new \App\Events\ChatMessageWasReceived($message, $user))->toOthers(); %% Разумеется, в нашем примере с Artisan-командой этот вызов ничего не сделает, но он сработает, если событие будет вызвано пользователем вашего приложения в активной сессии. == Вот и всё! == Всё это выглядит довольно просто, поэтому я хочу рассказать, в чём прелесть. Во-первых, обратите внимание, что сообщения, посылаемые пользователям, - это не просто текст, это JSON-представления ваших моделей. Чтобы понять, насколько это здорово, взгляните на то, как Тэйлор сделал менеджер задач, поддерживающий задачи в актуальном состоянии, на одной странице, в реальном времени в его ((https://laracasts.com/lessons/introducing-laravel-echo Laracasts видео)). Мощная вещь! Во-вторых, важно понимать, что **всё самое полезное Echo делает совершенно незаметно**. Вы можете согласиться, что это мощная вещь, и что она открывает множество возможностей, но при этом вы можете сказать: "Но Echo же ничего не делает!" Однако, вы не видите, как много требуется работы для настройки аутентификации, авторизации на каналах, обратных вызовов присутствия, и всего остального, что //делает для вас Echo//. Некоторые из этих функций есть в Pusher JS и в Socket.io с разными уровнями сложности, но в Echo они проще и согласованнее. А некоторых функций вообще нет в других библиотеках, по крайней мере в виде отдельной, простой функции. Echo берёт то, что может быть медленным и проблематичным в других сокет-библиотеках, и делает это лёгким и простым.