ЛР№10. Події, задачі та широкомовлення

Теоретический материал

1. События 

События Laravel обеспечивают простую реализацию шаблона Наблюдатель, позволяя вам подписываться и отслеживать различные события, происходящие в вашем приложении. Классы событий обычно хранятся в каталоге app/Events, а их слушатели – в app/Listeners. Не беспокойтесь, если вы не видите эти каталоги в своем приложении, так как они будут созданы для вас, когда вы будете генерировать события и слушатели с помощью команд консоли Artisan.

События служат отличным способом разделения различных аспектов вашего приложения, поскольку одно событие может иметь несколько слушателей, которые не зависят друг от друга. Например, бывает необходимо отправлять уведомление Slack своему пользователю каждый раз, когда заказ будет отправлен. Вместо того, чтобы связывать код обработки заказа с кодом уведомления Slack, вы можете вызвать событие App\Events\OrderShipped, которое слушатель может получить и использовать для отправки уведомления Slack.

Регистрация событий и слушателей

Поставщик App\Providers\EventServiceProvider Laravel – удобное место для регистрации всех слушателей событий вашего приложения. Свойство $listen содержит массив всех событий (ключей) и их слушателей (значений). Вы можете добавить в этот массив столько событий, сколько требуется вашему приложению. Например, добавим событие OrderShipped:

use App\Events\OrderShipped;
use App\Listeners\SendShipmentNotification;

/**
 * Карта слушателей событий приложения.
 *
 * @var array
 */
protected $listen = [
    OrderShipped::class => [
        SendShipmentNotification::class,
    ],
];
Команда event:list используется для отображения списка всех событий и слушателей, зарегистрированных вашим приложением.

Генерация событий и слушателей

Конечно, вручную создавать файлы для каждого события и слушателя сложно. Вместо этого добавьте необходимые события и их слушатели в поставщике EventServiceProvider, затем, используйте команду event:generate Artisan. Эта команда сгенерирует любые события или слушатели, перечисленные в поставщике EventServiceProvider, но которые еще не существуют:

php artisan event:generate

В качестве альтернативы вы можете использовать команды make:event и make:listener Artisan для генерации отдельных событий и слушателей:

php artisan make:event PodcastProcessed

php artisan make:listener SendPodcastNotification --event=PodcastProcessed

Ручная регистрация событий

Обычно события должны регистрироваться через массив $listen поставщика EventServiceProvider; но вы также можете явно зарегистрировать слушателей событий на основе классов или замыканий в методе boot вашего EventServiceProvider:

use App\Events\PodcastProcessed;
use App\Listeners\SendPodcastNotification;
use Illuminate\Support\Facades\Event;

/**
 * Регистрация любых событий вашего приложения.
 *
 * @return void
 */
public function boot()
{
    Event::listen(
        PodcastProcessed::class,
        [SendPodcastNotification::class, 'handle']
    );

    Event::listen(function (PodcastProcessed $event) {
        //
    });
}

Анонимные слушатели событий в очереди

При явной регистрации слушателей событий на основе замыкания вы можете обернуть замыкание слушателя в функцию Illuminate\Events\queueable, чтобы проинструктировать Laravel о выполнении слушателя с использованием очереди:

use App\Events\PodcastProcessed;
use function Illuminate\Events\queueable;
use Illuminate\Support\Facades\Event;

/**
 * Регистрация любых событий вашего приложения.
 *
 * @return void
 */
public function boot()
{
    Event::listen(queueable(function (PodcastProcessed $event) {
        //
    }));
}

Как и в случае с заданиями в очередях, вы можете использовать методы onConnectiononQueue и delay для детализации выполнения слушателя в очереди:

Event::listen(queueable(function (PodcastProcessed $event) {
    //
})->onConnection('redis')->onQueue('podcasts')->delay(now()->addSeconds(10)));

Если вы хотите обрабатывать сбои анонимного слушателя в очереди, то вы можете передать замыкание методу catch при определении слушателя queueable. Это замыкание получит экземпляр события и экземпляр Throwable, вызвавший сбой слушателя:

use App\Events\PodcastProcessed;
use function Illuminate\Events\queueable;
use Illuminate\Support\Facades\Event;
use Throwable;

Event::listen(queueable(function (PodcastProcessed $event) {
    //
})->catch(function (PodcastProcessed $event, Throwable $e) {
    // Событие в очереди завершилось неудачно ...
}));

Анонимные слушатели группы событий

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

Event::listen('event.*', function ($eventName, array $data) {
    //
});

2. Очереди

При создании веб-приложения у вас могут быть некоторые задачи, такие как синтаксический анализ и сохранение загруженного файла CSV, выполнение которых во время обычного веб-запроса занимает слишком много времени. К счастью, Laravel позволяет легко создавать задания (jobs) в очереди (queue), которые могут обрабатываться в фоновом режиме. Перемещая трудоемкие задания в очередь и выполняя их в фоне, ваше приложение может быстрее обрабатывать веб-запросы и быстрее отвечать клиенту.

Очереди Laravel предоставляют унифицированный API для различных серверных служб очередей, таких как Amazon SQSRedis или даже обычная реляционная база данных.

Параметры конфигурации очереди Laravel хранятся в файле конфигурации вашего приложения config/queue.php. В этом файле вы найдете конфигурации подключения для каждого из драйверов очереди фреймворка: база данных, Amazon SQSRedis и Beanstalkd, а также синхронный драйвер для немедленного выполнения задания (используется во время локальной разработки). Также имеется драйвер очереди null, который просто выбрасывает задания из очереди, не исполняя их.

Laravel также предлагает Horizon, красивую панель управления и систему конфигурации для ваших очередей с поддержкой Redis. Дополнительную информацию можно найти в полной документации Horizon.

Соединения и очереди

Прежде чем приступить к работе с очередями Laravel, важно понять различие между «соединениями» и «очередями». В конфигурационном файле config/queue.php есть массив connections. Этот параметр определяет подключения к серверным службам очередей, таким как Amazon SQS, Beanstalk или Redis. Однако любое указанное «соединение» очереди может иметь несколько «очередей», которые можно рассматривать как разные стеки или пачки поочередных заданий.

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

use App\Jobs\ProcessPodcast;

// Это задание отправляется в очередь `default` соединения по умолчанию ...
ProcessPodcast::dispatch();

// Это задание отправляется в очередь `emails` соединения по умолчанию ...
ProcessPodcast::dispatch()->onQueue('emails');

Некоторым приложениям может не понадобиться помещать задания в несколько очередей, вместо этого предпочитая иметь одну простую очередь. Однако отправка заданий в несколько очередей может быть особенно полезна для приложений, определяющих приоритеты или сегментацию процесса обработки заданий, поскольку обработчик очереди Laravel позволяет вам указать, какие очереди он должен обрабатывать по приоритету. Например, если вы помещаете задания в очередь high, то вы можете запустить обработчик, который даст им более высокий приоритет обработки:

php artisan queue:work --queue=high,default

Предварительная подготовка драйверов

База данных

Чтобы использовать драйвер очереди database, вам понадобится таблица базы данных для хранения заданий. Чтобы сгенерировать миграцию, которая создает эту таблицу, запустите команду queue:table Artisan. После того, как миграция будет создана, вы можете выполнить ее миграцию с помощью команды migrate:

php artisan queue:table

php artisan migrate

Redis

Чтобы использовать драйвер очереди redis, вы должны настроить соединение с базой данных Redis в файле конфигурации config/database.php.

Кластер Redis

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

'redis' => [
    'driver' => 'redis',
    'connection' => 'default',
    'queue' => '{default}',
    'retry_after' => 90,
],

Блокировка

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

Настройка этого значения зависит от загрузки очереди и может быть более эффективной, чем постоянный опрос базы данных Redis на предмет новых заданий. Например, вы можете установить значение 5, чтобы указать, что драйвер должен блокироваться на пять секунд, ожидая, пока задание станет доступным:

'redis' => [
    'driver' => 'redis',
    'connection' => 'default',
    'queue' => 'default',
    'retry_after' => 90,
    'block_for' => 5,
],
Установка для block_for значения 0 заставит обработчиков очереди блокироваться на неопределенный срок, пока задание не станет доступным. Это также предотвратит обработку таких сигналов, как SIGTERM, до тех пор, пока не будет обработано следующее задание.

Дополнительные зависимости драйверов

Для перечисленных драйверов очереди необходимы следующие зависимости. Эти зависимости могут быть установлены через менеджер пакетов Composer:

  • Amazon SQS: aws/aws-sdk-php ~3.0
  • Beanstalkd: pda/pheanstalk ~4.0
  • Redis: predis/predis ~1.0 or phpredis PHP extension

Создание заданий

Генерация класса задания

Чтобы сгенерировать новое задание, используйте команду make:job Artisan. Эта команда поместит новый класс задания в каталог app/Jobs вашего приложения. Если этот каталог не существует в вашем приложении, то Laravel предварительно создаст его:

php artisan make:job ProcessPodcast

Сгенерированный класс будет реализовывать интерфейс Illuminate\Contracts\Queue\ShouldQueue, указывая Laravel, что задание должно быть поставлено в очередь для асинхронного выполнения.

Заготовки (stub) заданий можно настроить с помощью публикации заготовок.

Структура класса задания

Классы заданий очень простые, обычно они содержат только метод handle, который вызывается, когда задание обрабатывается очередью. Для начала рассмотрим пример класса задания. В этом примере мы представим, что управляем службой публикации подкастов и нам необходимо обработать загруженные файлы подкастов перед их публикацией:

<?php

namespace App\Jobs;

use App\Models\Podcast;
use App\Services\AudioProcessor;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Bus\Dispatchable;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Queue\SerializesModels;

class ProcessPodcast implements ShouldQueue
{
    use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;

    /**
     * Экземпляр подкаста.
     *
     * @var \App\Models\Podcast
     */
    protected $podcast;

    /**
     * Создать новый экземпляр задания.
     *
     * @param  App\Models\Podcast  $podcast
     * @return void
     */
    public function __construct(Podcast $podcast)
    {
        $this->podcast = $podcast;
    }

    /**
     * Выполнить задание.
     *
     * @param  App\Services\AudioProcessor  $processor
     * @return void
     */
    public function handle(AudioProcessor $processor)
    {
        // Обработка загруженного подкаста ...
    }
}

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

Если ваше задание в очереди принимает модель Eloquent в своем конструкторе, в очередь будет сериализован только идентификатор модели. Когда задание действительно обрабатывается, система очередей автоматически повторно извлекает полный экземпляр модели и его загруженные отношения из базы данных. Такой подход к сериализации модели позволяет отправлять в драйвер очереди гораздо меньший объем данных.

Внедрение зависимости метода handle

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

Если вы хотите получить полный контроль над тем, как контейнер внедряет зависимости в метод handle, вы можете использовать метод bindMethod контейнера. Метод bindMethod принимает функцию, которая получает задание и контейнер. В функции вы можете вызывать метод handle. Обычно вы должны вызывать bindMethod из метода boot вашего сервис-провайдера App\Providers\AppServiceProvider:

use App\Jobs\ProcessPodcast;
use App\Services\AudioProcessor;

$this->app->bindMethod([ProcessPodcast::class, 'handle'], function ($job, $app) {
    return $job->handle($app->make(AudioProcessor::class));
});
Бинарные данные, например, необработанное содержимое изображения, должны быть переданы через функцию base64_encode перед передачей заданию. В противном случае задание может неправильно сериализоваться в JSON при отправки в очередь.

Обработка отношений

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

/**
 * Создать новый экземпляр задания.
 *
 * @param  \App\Models\Podcast  $podcast
 * @return void
 */
public function __construct(Podcast $podcast)
{
    $this->podcast = $podcast->withoutRelations();
}

3. Широковещание


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

Например, представьте, что ваше приложение может экспортировать данные пользователя в файл CSV и отправлять этот файл ему по электронной почте. Однако создание этого CSV-файла занимает несколько минут, поэтому вы можете создать и отправить CSV-файл по почте, поместив задание в очередь. Когда файл CSV будет создан и отправлен пользователю, тогда мы можем использовать широковещание для отправки события App\Events\UserDataExported, которое будет получено в JavaScript нашего приложения. Как только событие будет получено, мы можем отобразить сообщение пользователю о том, что его файл CSV был отправлен ему по электронной почте без необходимости в обновлении страницы.

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

Поддерживаемые драйверы

По умолчанию Laravel содержит два серверных драйвера трансляции на выбор: Pusher Channels и Ably. Однако пакеты сообщества, например, laravel-websockets, предлагают дополнительные драйверы трансляции без использования платных провайдеров.

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

Установка на стороне сервера

Чтобы начать использовать трансляцию событий Laravel, нам нужно выполнить некоторую настройку в приложении Laravel, а также установить некоторые пакеты.

Трансляция событий осуществляется серверным драйвером трансляции, который транслирует ваши события Laravel, получаемые браузером клиента через Laravel Echo (библиотека JavaScript). Не волнуйтесь – мы рассмотрим каждую часть процесса установки шаг за шагом.

Конфигурирование

Вся конфигурация трансляций событий вашего приложения хранится в конфигурационном файле config/broadcasting.php. Laravel из коробки поддерживает несколько драйверов трансляции: Pusher ChannelsRedis, и драйвер log для локальной разработки и отладки. Кроме того, поддерживается драйвер null, который позволяет полностью отключить трансляцию во время тестирования. В конфигурационном файле config/broadcasting.php содержится пример конфигурации для каждого из этих драйверов.

Поставщик службы трансляции

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

Конфигурирование очереди

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

Pusher Channels

Если вы планируете транслировать свои события с помощью Pusher Channels, то вам следует установить PHP SDK Pusher Channels с помощью менеджера пакетов Composer:

composer require pusher/pusher-php-server "^5.0"

Далее, вы должны настроить свои учетные данные Pusher Channels в конфигурационном файле config/broadcasting.php. Пример конфигурации Pusher Channels уже содержится в этом файле, что позволяет быстро указать параметры keysecret, и app_id. Как правило, эти значения должны быть установлены через переменные окружения PUSHER_APP_KEYPUSHER_APP_SECRET и PUSHER_APP_ID:

PUSHER_APP_ID=your-pusher-app-id
PUSHER_APP_KEY=your-pusher-key
PUSHER_APP_SECRET=your-pusher-secret
PUSHER_APP_CLUSTER=mt1

Конфигурация pusher в файле config/broadcasting.php также позволяет вам указывать дополнительные параметры, которые поддерживаются Pusher, например, cluster.

Далее, вам нужно будет изменить драйвер трансляции на pusher в файле .env:

BROADCAST_DRIVER=pusher

И, наконец, вы готовы установить и настроить Laravel Echo, который будет получать транслируемые события на клиентской стороне.


Завдання

1. Створити нову сутність відгук (feedback).
2. Використовуючи генератор, створити міграцію, модель, контролер.
3. Створити новій прослуховувач та зареестравути його для прослуховування події створення нового відгука.
4. Створити нуву задачу, що виконує відправку почтового повідомлення про отримання вдгуку її автору та повідомлення, яке отримує адміністратор сайту про наявність нового відгуку.
5. Відправити у чергу завдання з попереднього пункту у прослуховувачі створеному на 3 кроці.
6. Створити та налагодити широмовну подію що відпраляє загальну кількість відгуків використовуючи  сервіс PUSHER.
7. Створити та зареєструвати оглядач (Observer), що розсилає широкомовне повідомлення з кроку 6 у разі зміни кількості відгуків на сайті. 
8. На головній сторінці сайти створити блок, що відображую загальну кількість відгуків та оновлюєтся при отриманні Pusher повідомлень.

Контрольні питання

1. Що таке сокет?

2. Які види широкомовлення існують в ларавел?

3. Як можна переглянути задачі черги, що не виконані успішною?