- Введение
- Регистрация событий и слушателей
- Определение событий
- Определение слушателей
- Очередные слушатели событий
- Вызов событий
- Подписчики на события
- Тестирование
#Введение
События в Laravel реализуют простой паттерн наблюдателя, позволяя подписываться и слушать различные события, происходящие в вашем приложении. Классы событий обычно хранятся в каталоге app/Events, а их слушатели — в app/Listeners. Не беспокойтесь, если этих каталогов нет в вашем приложении — они будут созданы автоматически при генерации событий и слушателей с помощью Artisan-команд.
События отлично подходят для разделения различных аспектов приложения, так как одно событие может иметь несколько слушателей, которые не зависят друг от друга. Например, вы можете захотеть отправлять уведомление в Slack пользователю каждый раз, когда заказ отправлен. Вместо того чтобы связывать код обработки заказа с кодом уведомления Slack, вы можете вызвать событие App\Events\OrderShipped, которое слушатель получит и использует для отправки уведомления.
#Регистрация событий и слушателей
App\Providers\EventServiceProvider, входящий в состав Laravel, предоставляет удобное место для регистрации всех слушателей событий вашего приложения. Свойство listen содержит массив всех событий (ключи) и их слушателей (значения). Вы можете добавить в этот массив столько событий, сколько требуется вашему приложению. Например, добавим событие OrderShipped:
use App\Events\OrderShipped;
use App\Listeners\SendShipmentNotification;
/**
* Отображение слушателей событий для приложения.
*
* @var array<class-string, array<int, class-string>>
*/
protected $listen = [
OrderShipped::class => [
SendShipmentNotification::class,
],
];
Команда event:list позволяет вывести список всех событий и слушателей, зарегистрированных в вашем приложении.
#Генерация событий и слушателей
Разумеется, создавать файлы для каждого события и слушателя вручную неудобно. Вместо этого добавьте слушателей и события в EventServiceProvider и используйте Artisan-команду event:generate. Эта команда сгенерирует все события и слушатели, указанные в вашем EventServiceProvider, которых ещё нет:
php artisan event:generate
Кроме того, вы можете использовать Artisan-команды make:event и make:listener для генерации отдельных событий и слушателей:
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;
/**
* Зарегистрировать другие события для вашего приложения.
*/
public function boot(): void
{
Event::listen(
PodcastProcessed::class,
SendPodcastNotification::class,
);
Event::listen(function (PodcastProcessed $event) {
// ...
});
}
#Очередные анонимные слушатели событий
При ручной регистрации слушателей на основе замыканий вы можете обернуть замыкание в функцию Illuminate\Events\queueable, чтобы указать Laravel выполнять слушатель через очередь:
use App\Events\PodcastProcessed;
use function Illuminate\Events\queueable;
use Illuminate\Support\Facades\Event;
/**
* Зарегистрировать другие события для вашего приложения.
*/
public function boot(): void
{
Event::listen(queueable(function (PodcastProcessed $event) {
// ...
}));
}
Как и для очередных заданий, вы можете использовать методы onConnection, onQueue и 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 (string $eventName, array $data) {
// ...
});
#Автоматическое обнаружение событий
Вместо ручной регистрации событий и слушателей в массиве $listen EventServiceProvider вы можете включить автоматическое обнаружение событий. При включённом обнаружении Laravel автоматически найдёт и зарегистрирует ваши события и слушателей, сканируя каталог Listeners вашего приложения. При этом явно указанные события в EventServiceProvider также будут зарегистрированы.
Laravel находит слушателей, сканируя классы слушателей с помощью PHP Reflection. Когда Laravel обнаруживает метод класса слушателя, начинающийся с handle или __invoke, он регистрирует этот метод как слушатель для события, тип которого указан в сигнатуре метода:
use App\Events\PodcastProcessed;
class SendPodcastNotification
{
/**
* Обработать событие.
*/
public function handle(PodcastProcessed $event): void
{
// ...
}
}
Автоматическое обнаружение событий по умолчанию отключено, но вы можете включить его, переопределив метод shouldDiscoverEvents в вашем EventServiceProvider:
/**
* Определить, следует ли автоматически обнаруживать события и слушателей.
*/
public function shouldDiscoverEvents(): bool
{
return true;
}
По умолчанию сканируются все слушатели в каталоге app/Listeners. Если вы хотите добавить другие каталоги для сканирования, переопределите метод discoverEventsWithin в вашем EventServiceProvider:
/**
* Получить каталоги слушателей для обнаружения событий.
*
* @return array<int, string>
*/
protected function discoverEventsWithin(): array
{
return [
$this->app->path('Listeners'),
];
}
#Автоматическое обнаружение событий в продакшене
В продакшене неэффективно сканировать всех слушателей при каждом запросе. Поэтому в процессе деплоя следует выполнить Artisan-команду event:cache для кэширования манифеста всех событий и слушателей. Этот манифест ускорит процесс регистрации событий. Команда event:clear удаляет кэш.
#Определение событий
Класс события — это, по сути, контейнер данных, содержащий информацию, связанную с событием. Например, предположим, что событие App\Events\OrderShipped получает объект Eloquent ORM:
<?php
namespace App\Events;
use App\Models\Order;
use Illuminate\Broadcasting\InteractsWithSockets;
use Illuminate\Foundation\Events\Dispatchable;
use Illuminate\Queue\SerializesModels;
class OrderShipped
{
use Dispatchable, InteractsWithSockets, SerializesModels;
/**
* Создать новый экземпляр события.
*/
public function __construct(
public Order $order,
) {}
}
Как видите, этот класс события не содержит логики. Это контейнер для экземпляра App\Models\Order, который был приобретён. Трейт SerializesModels, используемый событием, корректно сериализует любые модели Eloquent, если объект события сериализуется с помощью PHP-функции serialize, например, при использовании очередных слушателей.
#Определение слушателей
Далее рассмотрим слушатель для нашего примера события. Слушатели получают экземпляры событий в методе handle. Artisan-команды event:generate и make:listener автоматически импортируют нужный класс события и укажут тип события в методе handle. Внутри handle вы можете выполнить любые действия, необходимые для обработки события:
<?php
namespace App\Listeners;
use App\Events\OrderShipped;
class SendShipmentNotification
{
/**
* Создать слушателя события.
*/
public function __construct()
{
// ...
}
/**
* Обработать событие.
*/
public function handle(OrderShipped $event): void
{
// Доступ к заказу через $event->order...
}
}
Ваши слушатели могут также указывать в конструкторах любые зависимости. Все слушатели разрешаются через service container Laravel, поэтому зависимости будут внедрены автоматически.
#Остановка распространения события
Иногда нужно остановить распространение события на другие слушатели. Для этого можно вернуть false из метода handle слушателя.
#Очередные слушатели событий
Очередь слушателей полезна, если слушатель выполняет долгую задачу, например, отправку письма или HTTP-запрос. Перед использованием очередных слушателей настройте очередь и запустите воркер очереди на сервере или локально.
Чтобы указать, что слушатель должен быть очередным, добавьте интерфейс ShouldQueue к классу слушателя. Слушатели, сгенерированные командами event:generate и make:listener, уже импортируют этот интерфейс, так что вы можете использовать его сразу:
<?php
namespace App\Listeners;
use App\Events\OrderShipped;
use Illuminate\Contracts\Queue\ShouldQueue;
class SendShipmentNotification implements ShouldQueue
{
// ...
}
Готово! Теперь, когда событие, обрабатываемое этим слушателем, будет отправлено, диспетчер событий автоматически поместит слушатель в очередь, используя систему очередей Laravel. Если при выполнении слушателя в очереди не возникнет исключений, задача в очереди будет автоматически удалена после завершения обработки.
#Настройка подключения, имени очереди и задержки
Если вы хотите настроить подключение к очереди, имя очереди или время задержки для слушателя, определите свойства $connection, $queue или $delay в классе слушателя:
<?php
namespace App\Listeners;
use App\Events\OrderShipped;
use Illuminate\Contracts\Queue\ShouldQueue;
class SendShipmentNotification implements ShouldQueue
{
/**
* Имя подключения, к которому должно быть отправлено задание.
*
* @var string|null
*/
public $connection = 'sqs';
/**
* Имя очереди, в которую должно быть отправлено задание.
*
* @var string|null
*/
public $queue = 'listeners';
/**
* Время (в секундах) до обработки задания.
*
* @var int
*/
public $delay = 60;
}
Если вы хотите определить подключение, имя очереди или задержку слушателя во время выполнения, вы можете определить методы viaConnection, viaQueue или withDelay в классе слушателя:
/**
* Получить имя подключения очереди слушателя.
*/
public function viaConnection(): string
{
return 'sqs';
}
/**
* Получить имя очереди слушателя.
*/
public function viaQueue(): string
{
return 'listeners';
}
/**
* Получить количество секунд до обработки задания.
*/
public function withDelay(OrderShipped $event): int
{
return $event->highPriority ? 0 : 60;
}
#Условная постановка слушателей в очередь
Иногда нужно определить, должен ли слушатель быть поставлен в очередь на основе данных, доступных только во время выполнения. Для этого в слушатель можно добавить метод shouldQueue, который решает, ставить ли слушатель в очередь. Если метод shouldQueue возвращает false, слушатель выполнен не будет:
<?php
namespace App\Listeners;
use App\Events\OrderCreated;
use Illuminate\Contracts\Queue\ShouldQueue;
class RewardGiftCard implements ShouldQueue
{
/**
* Наградить подарочной картой клиента.
*/
public function handle(OrderCreated $event): void
{
// ...
}
/**
* Определить, должен ли слушатель ставиться в очередь.
*/
public function shouldQueue(OrderCreated $event): bool
{
return $event->order->subtotal >= 5000;
}
}
#Ручное взаимодействие с очередью
Если нужно вручную получить доступ к методам delete и release базового задания очереди слушателя, используйте трейт Illuminate\Queue\InteractsWithQueue. Этот трейт импортируется по умолчанию в сгенерированных слушателях и предоставляет доступ к этим методам:
<?php
namespace App\Listeners;
use App\Events\OrderShipped;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Queue\InteractsWithQueue;
class SendShipmentNotification implements ShouldQueue
{
use InteractsWithQueue;
/**
* Обработать событие.
*/
public function handle(OrderShipped $event): void
{
if (true) {
$this->release(30);
}
}
}
#Очередные слушатели и транзакции базы данных
Когда очередные слушатели вызываются внутри транзакций базы данных, они могут быть обработаны очередью до того, как транзакция будет зафиксирована. В этом случае изменения моделей или записей, сделанные в транзакции, могут ещё не появиться в базе. Кроме того, модели или записи, созданные в транзакции, могут отсутствовать в базе. Если слушатель зависит от этих моделей, при обработке задания могут возникнуть неожиданные ошибки.
Если в настройках подключения очереди параметр after_commit установлен в false, вы всё равно можете указать, что конкретный очередной слушатель должен вызываться после фиксации всех открытых транзакций, реализовав интерфейс ShouldHandleEventsAfterCommit в классе слушателя:
<?php
namespace App\Listeners;
use Illuminate\Contracts\Events\ShouldHandleEventsAfterCommit;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Queue\InteractsWithQueue;
class SendShipmentNotification implements ShouldQueue, ShouldHandleEventsAfterCommit
{
use InteractsWithQueue;
}
Чтобы узнать больше о решении этих проблем, ознакомьтесь с документацией по очередным заданиям и транзакциям базы данных.
#Обработка неудачных заданий
Иногда очередные слушатели могут завершиться с ошибкой. Если слушатель превысит максимальное число попыток, заданное вашим воркером очереди, будет вызван метод failed слушателя. Метод failed получает экземпляр события и объект Throwable, вызвавший ошибку:
<?php
namespace App\Listeners;
use App\Events\OrderShipped;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Queue\InteractsWithQueue;
use Throwable;
class SendShipmentNotification implements ShouldQueue
{
use InteractsWithQueue;
/**
* Обработать событие.
*/
public function handle(OrderShipped $event): void
{
// ...
}
/**
* Обработать ошибку задания.
*/
public function failed(OrderShipped $event, Throwable $exception): void
{
// ...
}
}
#Указание максимального числа попыток очередного слушателя
Если один из ваших очередных слушателей вызывает ошибку, скорее всего, вы не захотите, чтобы он пытался выполняться бесконечно. Поэтому Laravel предоставляет способы указать, сколько раз или как долго слушатель может пытаться выполниться.
Вы можете определить свойство $tries в классе слушателя, чтобы указать, сколько раз слушатель может пытаться выполниться до того, как будет считаться неудачным:
<?php
namespace App\Listeners;
use App\Events\OrderShipped;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Queue\InteractsWithQueue;
class SendShipmentNotification implements ShouldQueue
{
use InteractsWithQueue;
/**
* Количество попыток выполнения очередного слушателя.
*
* @var int
*/
public $tries = 5;
}
Вместо указания количества попыток вы можете определить время, после которого слушатель больше не будет пытаться выполниться. Это позволяет слушателю пытаться неограниченное число раз в течение заданного периода. Для этого добавьте метод retryUntil в класс слушателя. Метод должен возвращать объект DateTime:
use DateTime;
/**
* Определить время, после которого слушатель перестанет пытаться выполниться.
*/
public function retryUntil(): DateTime
{
return now()->addMinutes(5);
}
#Вызов событий
Чтобы вызвать событие, можно использовать статический метод dispatch события. Этот метод доступен благодаря трейту Illuminate\Foundation\Events\Dispatchable. Все аргументы, переданные в dispatch, будут переданы в конструктор события:
<?php
namespace App\Http\Controllers;
use App\Events\OrderShipped;
use App\Http\Controllers\Controller;
use App\Models\Order;
use Illuminate\Http\RedirectResponse;
use Illuminate\Http\Request;
class OrderShipmentController extends Controller
{
/**
* Отправить заказ.
*/
public function store(Request $request): RedirectResponse
{
$order = Order::findOrFail($request->order_id);
// Логика отправки заказа...
OrderShipped::dispatch($order);
return redirect('/orders');
}
}
Если нужно условно вызвать событие, используйте методы dispatchIf и dispatchUnless:
OrderShipped::dispatchIf($condition, $order);
OrderShipped::dispatchUnless($condition, $order);
При тестировании полезно проверять, что события были вызваны, не выполняя их слушателей. Встроенные тестовые помощники Laravel облегчают это.
#Вызов событий после транзакций базы данных
Иногда нужно, чтобы Laravel вызывал событие только после фиксации текущей транзакции базы данных. Для этого реализуйте интерфейс ShouldDispatchAfterCommit в классе события.
Этот интерфейс указывает Laravel не вызывать событие до тех пор, пока текущая транзакция не будет зафиксирована. Если транзакция не удалась, событие будет отброшено. Если при вызове события транзакция не активна, событие будет вызвано сразу:
<?php
namespace App\Events;
use App\Models\Order;
use Illuminate\Broadcasting\InteractsWithSockets;
use Illuminate\Contracts\Events\ShouldDispatchAfterCommit;
use Illuminate\Foundation\Events\Dispatchable;
use Illuminate\Queue\SerializesModels;
class OrderShipped implements ShouldDispatchAfterCommit
{
use Dispatchable, InteractsWithSockets, SerializesModels;
/**
* Создать новый экземпляр события.
*/
public function __construct(
public Order $order,
) {}
}
#Подписчики на события
#Создание подписчиков на события
Подписчики — это классы, которые могут подписываться на несколько событий внутри самого класса, позволяя определить несколько обработчиков событий в одном классе. Подписчики должны определить метод subscribe, которому передаётся экземпляр диспетчера событий. Вы можете вызвать метод listen у переданного диспетчера для регистрации слушателей:
<?php
namespace App\Listeners;
use Illuminate\Auth\Events\Login;
use Illuminate\Auth\Events\Logout;
use Illuminate\Events\Dispatcher;
class UserEventSubscriber
{
/**
* Обработать событие входа пользователя.
*/
public function handleUserLogin(Login $event): void {}
/**
* Обработать событие выхода пользователя.
*/
public function handleUserLogout(Logout $event): void {}
/**
* Зарегистрировать слушателей для подписчика.
*/
public function subscribe(Dispatcher $events): void
{
$events->listen(
Login::class,
[UserEventSubscriber::class, 'handleUserLogin']
);
$events->listen(
Logout::class,
[UserEventSubscriber::class, 'handleUserLogout']
);
}
}
Если методы слушателей определены внутри самого подписчика, удобнее возвращать из метода subscribe массив событий и имён методов. Laravel автоматически определит имя класса подписчика при регистрации слушателей:
<?php
namespace App\Listeners;
use Illuminate\Auth\Events\Login;
use Illuminate\Auth\Events\Logout;
use Illuminate\Events\Dispatcher;
class UserEventSubscriber
{
/**
* Обработать событие входа пользователя.
*/
public function handleUserLogin(Login $event): void {}
/**
* Обработать событие выхода пользователя.
*/
public function handleUserLogout(Logout $event): void {}
/**
* Зарегистрировать слушателей для подписчика.
*
* @return array<string, string>
*/
public function subscribe(Dispatcher $events): array
{
return [
Login::class => 'handleUserLogin',
Logout::class => 'handleUserLogout',
];
}
}
#Регистрация подписчиков на события
После создания подписчика вы готовы зарегистрировать его в диспетчере событий. Подписчики регистрируются через свойство $subscribe в EventServiceProvider. Например, добавим UserEventSubscriber в список:
<?php
namespace App\Providers;
use App\Listeners\UserEventSubscriber;
use Illuminate\Foundation\Support\Providers\EventServiceProvider as ServiceProvider;
class EventServiceProvider extends ServiceProvider
{
/**
* Отображение слушателей событий для приложения.
*
* @var array
*/
protected $listen = [
// ...
];
/**
* Классы подписчиков для регистрации.
*
* @var array
*/
protected $subscribe = [
UserEventSubscriber::class,
];
}
#Тестирование
При тестировании кода, вызывающего события, может потребоваться запретить выполнение слушателей, так как код слушателей можно тестировать отдельно. Для тестирования слушателя вы можете создать его экземпляр и вызвать метод handle напрямую.
С помощью метода fake фасада Event вы можете предотвратить выполнение слушателей, выполнить тестируемый код и затем проверить, какие события были вызваны, используя методы assertDispatched, assertNotDispatched и assertNothingDispatched:
<?php
namespace Tests\Feature;
use App\Events\OrderFailedToShip;
use App\Events\OrderShipped;
use Illuminate\Support\Facades\Event;
use Tests\TestCase;
class ExampleTest extends TestCase
{
/**
* Тест отправки заказа.
*/
public function test_orders_can_be_shipped(): void
{
Event::fake();
// Выполнить отправку заказа...
// Проверить, что событие было вызвано...
Event::assertDispatched(OrderShipped::class);
// Проверить, что событие было вызвано дважды...
Event::assertDispatched(OrderShipped::class, 2);
// Проверить, что событие не было вызвано...
Event::assertNotDispatched(OrderFailedToShip::class);
// Проверить, что события не были вызваны...
Event::assertNothingDispatched();
}
}
Вы можете передать замыкание в методы assertDispatched или assertNotDispatched, чтобы проверить, что событие было вызвано и прошло заданную проверку. Если хотя бы одно событие прошло проверку, утверждение будет успешным:
Event::assertDispatched(function (OrderShipped $event) use ($order) {
return $event->order->id === $order->id;
});
Если вы хотите просто проверить, что слушатель слушает определённое событие, используйте метод assertListening:
Event::assertListening(
OrderShipped::class,
SendShipmentNotification::class
);
После вызова Event::fake() никакие слушатели событий не будут выполнены. Поэтому, если ваши тесты используют фабрики моделей, которые зависят от событий, например, создание UUID при событии creating модели, вызывайте Event::fake() после использования фабрик.
#Фальсификация части событий
Если нужно фальсифицировать слушателей только для определённого набора событий, передайте их в метод fake или fakeFor:
/**
* Тест обработки заказа.
*/
public function test_orders_can_be_processed(): void
{
Event::fake([
OrderCreated::class,
]);
$order = Order::factory()->create();
Event::assertDispatched(OrderCreated::class);
// Другие события вызываются как обычно...
$order->update([...]);
}
Вы можете фальсифицировать все события, кроме указанных, используя метод except:
Event::fake()->except([
OrderCreated::class,
]);
#Локальная фальсификация событий
Если нужно фальсифицировать слушателей только для части теста, используйте метод fakeFor:
<?php
namespace Tests\Feature;
use App\Events\OrderCreated;
use App\Models\Order;
use Illuminate\Support\Facades\Event;
use Tests\TestCase;
class ExampleTest extends TestCase
{
/**
* Test order process.
*/
public function test_orders_can_be_processed(): void
{
$order = Event::fakeFor(function () {
$order = Order::factory()->create();
Event::assertDispatched(OrderCreated::class);
return $order;
});
// События вызываются как обычно, и наблюдатели работают...
$order->update([...]);
}
}