Магия Событий, Слушателей и Задач

Магия Событий, Слушателей и Задач

В вашем приложении есть три вещи, которые можно запушить в фоновый режим и запустить независимо от жизненного цикла Запрос-Ответ:
Слушатели (Listeners), Задачи (Jobs) и Уведомления (Notifications).

Для простоты я буду относиться к Слушателям и Подписчикам (Subscribers) как к одному и тому же, поскольку они прикреплены к Событию (Event), и единственное практическое различие между ними заключается в том, что подписчик может прослушивать несколько Событий, а Слушатель — только одно.

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

Очередь (Queue) в основном занимается «делай Х как можно скорее, потому что Y пушит ее». В то время как, Планировщик задач (Scheduled Jobs) — «делай X, потому что пришло время сделать это».

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

Но, даже если это можно поставить в очередь, Задачи и Слушатели работают немного по-разному, поскольку различается их назначение.

Только Задачи могут быть запланированы

Одно из основных различий между Задачами, Слушателями и Уведомлениями заключается в том, что Задачи могут быть запланированы (Scheduled), а Слушатели и Уведомления — нет. Они могут быть отложены, то есть Слушатель или Уведомление будут помещены в очередь, но будут обработаны позже.

Так как они связаны с Событием (что-то произошло), очень трудно и почти контрпродуктивно использовать их без Событий для прослушивания. Метод handle() Слушателей автоматически получает Событие, которое он прослушивает. Фреймворк делает это автоматически при возникновении события.

Если вы попытаетесь запланировать Задачу События, то она с треском провалится, так как не получит его. Единственный способ сделать это — вручную внедрить Событие, а внутри события внедрить экземпляр того, что вы распространяете через шину Событий (Event Bus). Даже если бы вы могли сделать так, это все равно что водить машину в Uber и говорить, что вы сам себе начальник, то есть лгать самому себе:

$podcast = Podcast::find(2);

$event = new PodcastUploaded($podcast);

$this->schedule($event)->weekly(); 

// Вместе зачитаем рэп:
// F*ck the police comin' straight from the underground...

Уведомления, с другой стороны, будут получать экземпляр Notifiable, который по умолчанию реализован в модели User, кроме случаев когда вы не используете фасад Notification и маршрутизируете его вручную — за кулисами создается экземпляр AnonymousNotifiable, реализующий тот же экземпляр Notifiable, который передается в Уведомление, поэтому он и не паникует, как моя собака нашедшая паука.

Слушатели vs Задачи

Еще одно ключевое отличие Задачи от Слушателя заключается в том, что Задача может получать все что угодно при создании экземпляра, вы можете тайпхинтить вашу службу в методе handle(), чтобы Laravel её внедрил.

А Слушатель не может. Вместо метода handle(), используемый для События, вам нужно будет тайпхинтить любую службу, которая может понадобиться Слушателю в методе __construct(), который фреймворк будет обрабатывать автоматически.

namespace App\Listeners;

use App\Events\PodcastUploaded;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Bus\Queueable;

class UploadListener implements ShouldQueue
{
    use InteractsWithQueue, 
        Queueable;    
        
    /**
     * Транскодер подкаста
     *
     * @var \AudioMagic\AudioTranscoder
     */
    protected $transcoder;    
    
    
    /**
     * Создать Слушателя События
     *
     * @param  \AudioMagic\AudioTranscoder $transcoder
     * @return void
     */
    public function __construct(AudioTranscoder $transcoder)
    {
        $this->transcoder = $transcoder;
    }

    /**
     * Обработка события
     *
     * @param  \App\Events\PodcastUploaded $event
     * @return void
     */
    public function handle(PodcastUploaded $event)
    {
        $transcoded = $this->transcoder->process()
            ->format('mp3')
            ->bitrate('128')
            ->file($event->podcast->path)
            ->output($event->podcast->path . '_128.mp3')
            ->start();        
            
        if ($transcoded->success()) {
            // Что-нибудь сделать с с перекодированным файлом
        }        
        
        // ...    
   }
}

Свяжем всё

Чудо Laravel и самого PHP заключаются в том, что вы не привязаны к строгому и уникальному решению конкретной проблемы. Вы можете связать несколько Задач, Событий и Слушателей — единственное, что не может подцепить что-либо, — это Уведомление… в некотором смысле.

Как видите, отношения между ними очень динамичны. Я имею в виду, что вы можете установить Задачу, которое отправляет Уведомления и запускает Событие, полученное Слушателем… ну, вы поняли идею.

Например, вы можете связать это:

  • Когда пользователь загрузит файл Подкаста, мы отправим ProcessPodcastJob для его обработки.
  • Когда обработка успешно завершится, Задача запустит событие PodcastProcessed.
  • AddNewPodcastToHomeListener будет прослушивать это событие. Он добавит новый подкаст на главную странице с классным изображением, описанием и ссылкой.
  • Другой NotifyNewPublishedPodcast будет отвечать за рассылку уведомлений: PodcastPublishedNotification для пользователя, который его загрузил, и NewSubscribedPodcastNotification для пользователей, подписавшихся на подкаст, чтобы они могли сразу его прослушать.
Эффект бабочки. Один загруженный подкаст может запустить кучу логики в приложении.

Задача вызвала Событие, это Событие было обнаружено двумя Слушателями, и один из них использовал Уведомления. Конечно, это всего лишь пример, большая часть этого может быть сокращена в один класс Job, но, если код начнет расти, и вам нужно будет разделить происходящее по своим очередям, этот пример может стать очень полезным.

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

Автор: Italo Baeza
Перевод: Demiurge Ash