1. Глубже в детали
  2. События

Введение

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

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

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

Включенный в ваше Laravel-приложение App\Providers\EventServiceProvider предоставляет удобное место для регистрации всех слушателей событий вашего приложения. Свойство 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 при определении слушателя в очереди. Это замыкание получит экземпляр события и экземпляр 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, все равно будут зарегистрированы.

Обнаружение событий отключено по умолчанию, но вы можете включить его, переопределив метод shouldDiscoverEvents вашего провайдера событий приложения:

use App\Events\PodcastProcessed;
 
class SendPodcastNotification
{
/**
* Обработать заданное событие.
*/
public function handle(PodcastProcessed $event): void
{
// ...
}
}

Обнаружение событий отключено по умолчанию, но вы можете включить его, переопределив метод shouldDiscoverEvents в провайдере событий вашего приложения:

/**
* Определить, должны ли события и слушатели автоматически обнаруживаться.
*/
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, если объект события сериализуется с использованием функции serialize PHP, например, при использовании очередных слушателей.

Определение слушателей

Теперь давайте взглянем на слушатель нашего примера события. Слушатели событий получают экземпляры событий в своем методе 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...
}
}

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

Прекращение распространения события

Иногда вам может потребоваться прекратить распространение события на другие слушатели. Вы можете сделать это, вернув false из метода handle вашего слушателя.

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

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

Чтобы указать, что слушатель должен быть помещен в очередь, добавьте интерфейс ShouldQueue в класс слушателя. Слушатели, созданные с использованием команд Artisan 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,
];
}

Тестирование

При тестировании кода, который отправляет события, вы можете захотеть указать Laravel не выполнять фактически слушатели событий, поскольку код слушателя может быть протестирован напрямую и отдельно от кода, который отправляет соответствующее событие. Конечно, чтобы протестировать сам слушатель, вы можете создать экземпляр слушателя и вызвать метод 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
{
/**
* Тестирование обработки заказа.
*/
public function test_orders_can_be_processed(): void
{
$order = Event::fakeFor(function () {
$order = Order::factory()->create();
 
Event::assertDispatched(OrderCreated::class);
 
return $order;
});
 
// События обрабатываются как обычно, и наблюдатели будут выполняться...
$order->update([...]);
}
}