События в 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" даёт нам возможность более реалистично проектировать приложения. Поднятие событий в сочетании с очередями заданий означает, что мы можем выбросить событие и продолжить работу с программой, а слушатели событий обрабатывают информацию вне основного потока. Это очень удобный паттерн, который можно иметь в своём распоряжении!