События в Laravel
При возникновении события все зарегистрированные слушатели запускаются и получают возможность действовать в соответствии с событием.
Подумайте об этом так — мама объявляет всему дому: Пора идти!
. Предполагая, что дети обратили на это внимание, начинается череда действий: Джек выключает свет, Лорен запирает входную дверь, Энди зовёт собаку со двора, а дети начинают усаживаться в машину.
Все родители, читающие это, знают, что на самом деле все происходит не так, но вы должны уловить суть. Объявление сделано (событие отправлено), и события начинают происходить (слушатели событий реагируют).
Если вы хотите получить подробную информацию о функциональности, связанной с событиями в Laravel, просмотрите страницу документации. Она действительно хороша. Однако эта статья не предназначена для замены документации. Вместо этого я хочу поделиться некоторыми способами использования событий в повседневной работе.
Понимание типов событий
Три основных типа событий, которые я использую, — это мои собственные события, нативные события и иногда события сторонних пакетов.
Нативные события запускаются фреймворком при успешной аутентификации, создании моделей Eloquent, а также в определённые моменты цикла запросов приложения.
Аналогично, сторонние пакеты могут инициировать события, позволяя мне регистрировать слушателей для реагирования на них.
Пользовательские события связаны с уникальной логикой работы приложения, например, TeacherHasCreatedNewClassroom или StudentHasCompletedAnAssignment. Именно эти события я чаще всего использую.
Попробуйте выполнить
php artisan event:list, чтобы увидеть все события, зарегистрированные в вашем приложении!
Реализация событий и слушателей событий
Laravel предоставляет так много ресурсов, связанных с событиями, что в них можно запутаться. Мне было полезно запомнить, что в структуре событий есть три основных участника:
1. Класс события
Классы событий — это простые PHP-классы с описательным именем. Мне нравится использовать прошедшее время того, что мы описываем, в форме: {Actor}Has{ActionDescription}. Эти классы могут принимать параметры конструктора, но это не обязательно.
class StudentHasJoinedClassroom
{
  public function __construct(public Student $student)
  {
  }
}2. Слушатели события
Слушатели называются так же, как и события, но в них обычно используются более ориентированные на действия имена, например NotifyStudentOfNewAssignment или WelcomeStudentToNewClassroom. Некоторые добавляют Listener в конец классов слушателей, но мне кажется, что это слишком многословно.
Они имеют единственный метод handle, который принимает экземпляр события, вызвавшего его.
class WelcomeStudentToNewClassroom
{
  public function handle(StudentHasJoinedClassroom $event): void
  {
    // Сделайте что-нибудь, чтобы поприветствовать студента
  }
}3. Регистрация события
Это ключевой элемент, который сопоставляет слушателей с конкретными событиями. Самый простой способ реализовать это — использовать EventServiceProvider, который содержит массив имён классов событий и соответствующих им слушателей.
protected $listen = [
  StudentHasJoinedClassroom::class => [
    WelcomeStudentToNewClassroom::class,
    AssignDefaultHomework::class,
  ],
]Существует несколько других методов регистрации слушателей события, но я предпочитаю этот способ, поскольку он позволяет легко открыть один файл и увидеть все сопоставления в одном месте.
Очередь слушателей события
По умолчанию слушатели будут обрабатываться в том порядке, в котором они были зарегистрированы. Это может быть необходимо в зависимости от ситуации, однако одним из преимуществ использования подхода "событие/слушатель" является то, что слушатели могут обрабатываться асинхронно в одной из очередей заданий Laravel. Поскольку количество слушателей, которые может вызвать событие, не ограничено, их последовательный запуск приведёт к замедлению времени отклика. Если слушатели не зависят друг от друга, мы можем поместить их в очередь для обработки при наличии свободных воркеров.
Это достигается путём добавления к слушателю интерфейса Illuminate\Contracts\Queue\ShouldQueue.
use Illuminate\Contracts\Queue\ShouldQueue;
class QueuedEventListenerExample implements ShouldQueue
{
  public function handle($event): void
  {
    //
  }
}Если предположить, что ваша очередь обрабатывает задания, то теперь этот слушатель будет вынесен за пределы основного выполнения, что позволит приложению быстрее получать ответы.
Стоит отметить, что слушатели событий в очереди могут быть обработаны в любой момент после того, как они были помещены в очередь, поэтому сделайте свой метод обработки достаточно интеллектуальным, чтобы обрабатывать изменения данных, произошедшие во время ожидания.
Если слушатели работают синхронно, то возврат
falseиз методаhandleодного из слушателей приведёт к остановке цепочки.
События модели
До сих пор мы концентрировались на пользовательских событиях, но в Laravel существует ещё один тип событий, называемый событиями модели. Они удобны, когда нам нужно реагировать на события, происходящие с нашими моделями Eloquent. Полный список событий приведён в документации, но основная идея заключается в том, что Laravel запускает события до и после того как модель была создана, обновлена, удалена и т.д.
Допустим, нам необходимо выполнить некоторую очистку данных при удалении модели, например, удалить аудитории, студентов и задания, когда преподаватель удаляет свою учётную запись.
Мы можем вызвать собственное событие TeacherHasBeenDeleted и назначить слушателей для обработки дополнительных удалений. Или же мы можем подключиться к собственному событию Deleting и запускать наши слушатели при его возникновении.
События модели — это то же самое, что и пользовательские события, за исключением того, что Laravel запускает их без нашего участия.
Действия при событиях модели
Для того чтобы выполнять действия/экшены при наступлении событий модели, мы должны сообщить Laravel, какой код должен быть выполнен.
Обратные вызовы загрузки
Если код слушателя прост, мы можем вызвать метод boot в модели Eloquent, запускаемый при наступлении события connected. Это выглядит следующим образом:
class Teacher extends Model
{
  public static function boot()
  {
      parent::boot();
      static::deleting(function (Model $teacher) {
          // инициировать удаление соответствующих ресурсов
      });
  }
}Таким образом может быть зарегистрировано любое из событий модели.
Наблюдатели модели
Если у нас много кода в загрузочных хуках, то может оказаться полезным вынести обратные вызовы в отдельный файл. Мы можем создать ещё один класс, называемый "наблюдатель"/"observer".
class TeacherObserver
{
    public function deleting(Teacher $teacher): void
    {
        // инициировать удаление соответствующих ресурсов
    }А затем мы должны зарегистрировать наблюдателя в EventServiceProvider в таком виде:
protected $observers = [
    Teacher::class => [TeacherObserver::class],
];Я предпочитаю не использовать наблюдатели, поскольку о них легко забыть, так как они находятся далеко за кулисами. Просто взглянув на модель Teacher, мы не увидим никаких намёков на то, что с событием удаления что-то происходит. Мы должны знать, что существует зарегистрированный наблюдатель.
$dispatchesEvents
Добавление свойства $dispatchesEvents к нашей модели даёт возможность подключить пользовательский класс событий к нативным событиям модели.
class Teacher extends Model
{
  protected $dispatchesEvents = [
    'deleting' => TeacherIsBeingDeleted::class,
  ];
}После того как мы указали Laravel вызывать событие TeacherIsBeingDeleted при удалении модели Teacher, мы можем использовать массив $listen провайдера EventServiceProvider для запуска слушателей, как и в случае с пользовательским событием.
Подписчики событий
Для организации событий и слушателей модели я предпочитаю использовать подписчиков событий. Этот подход сочетает в себе ясность EventServiceProvider, чистую абстракцию наблюдателей и наглядность $dispatchesEvents.
Начнём с определения пользовательских событий, которые должны запускаться после нативных событий модели. См. пример $dispatchesEvents выше.
После подключения пользовательских событий мы можем создать нового подписчика. Подписчик содержит обязательный метод subscribe, сопоставляющий классы событий с методами внутри класса.
class TeacherSubscriber
{
  public function handleDeletionAssociatedResources(TeacherIsDeleting $event): void
  {
     // инициировать удаление соответствующих ресурсов
  }
  public function subscribe(Dispatcher $events): array
  {
    return [
      TeacherIsDeleting::class => 'handleDeletionOfAssociatedResources'
    ];
  }
}Наконец, подписчик регистрируется в EventServiceProvider, где я обычно смотрю, какие события обрабатываются.
class EventServiceProvider extends ServiceProvider
{
  protected $subscribe = [
    TeacherSubscriber::class,
  ];
}Таким образом, удобно открыть модель Teacher, посмотреть на свойство $dispatchesEvents, чтобы увидеть, на какие события мы реагируем, посетить EventServiceProvider, чтобы увидеть, что обрабатывает события, а затем обратиться к TeacherSubscriber, чтобы получить конкретные данные.
В заключение
Отказ от линейного мышления "шаг 1 → шаг 2 → шаг 3" даёт нам возможность более реалистично проектировать приложения. Поднятие событий в сочетании с очередями заданий означает, что мы можем выбросить событие и продолжить работу с программой, а слушатели событий обрабатывают информацию вне основного потока. Это очень удобный паттерн, который можно иметь в своём распоряжении!