Події
Вступ
Події Laravel надають просту реалізацію патерну «спостерігач», яка дозволяє підписуватися на різні події вашого застосунку й слухати їх. Класи подій зазвичай зберігаються в каталозі app/Events, а їхні слухачі - в app/Listeners. Не переймайтеся, якщо цих каталогів у вашому застосунку немає: їх буде створено, коли ви генеруватимете події та слухачів консольними командами Artisan.
Події - чудовий спосіб розчепити різні частини застосунку, адже одна подія може мати кілька слухачів, які не залежать одне від одного. Наприклад, ви можете хотіти надсилати користувачеві сповіщення у Slack щоразу, коли замовлення відправлено. Замість зчіплювати код обробки замовлень із кодом сповіщень у Slack, ви можете підняти подію App\Events\OrderShipped, яку отримає слухач і надішле сповіщення у Slack.
Генерація подій і слухачів
Щоб швидко згенерувати події та слухачів, скористайтеся командами Artisan make:event і make:listener:
php artisan make:event PodcastProcessed
php artisan make:listener SendPodcastNotification --event=PodcastProcessed
Для зручності ви можете викликати команди Artisan make:event і make:listener і без додаткових аргументів. Тоді Laravel сам запитає назву класу, а для слухача - подію, яку той має слухати:
php artisan make:event
php artisan make:listener
Реєстрація подій і слухачів
Виявлення подій
За замовчуванням Laravel автоматично знаходить і реєструє ваших слухачів подій, скануючи каталог Listeners вашого застосунку. Коли Laravel знаходить у класі слухача метод, назва якого починається з handle або __invoke, він реєструє ці методи як слухачів події, тип якої вказано в сигнатурі методу:
use App\Events\PodcastProcessed;
class SendPodcastNotification
{
/**
* Handle the event.
*/
public function handle(PodcastProcessed $event): void
{
// ...
}
}
Ви можете слухати кілька подій за допомогою об'єднаних типів PHP:
/**
* Handle the event.
*/
public function handle(PodcastProcessed|PodcastPublished $event): void
{
// ...
}
Якщо ви плануєте зберігати слухачів в іншому каталозі чи в кількох каталогах, вкажіть Laravel сканувати ці каталоги методом withEvents у файлі bootstrap/app.php вашого застосунку:
->withEvents(discover: [
__DIR__.'/../app/Domain/Orders/Listeners',
])
Ви можете шукати слухачів у кількох схожих каталогах, скориставшись символом * як підстановкою:
->withEvents(discover: [
__DIR__.'/../app/Domain/*/Listeners',
])
Команда event:list дозволяє переглянути всіх слухачів, зареєстрованих у вашому застосунку:
php artisan event:list
Виявлення подій у продакшені
Щоб пришвидшити застосунок, вам варто закешувати маніфест усіх слухачів командами Artisan optimize або event:cache. Зазвичай цю команду виконують у межах процесу розгортання вашого застосунку. Фреймворк використовуватиме цей маніфест, щоб пришвидшити реєстрацію подій. Команда event:clear дозволяє знищити кеш подій.
Динамічне виявлення подій
Щоб динамічно керувати тим, чи буде виявлено конкретного слухача, реалізуйте в класі слухача інтерфейс ShouldBeDiscovered і опишіть метод shouldBeDiscovered, який повертає булеве значення. Якщо метод поверне false, слухача не буде зареєстровано під час виявлення подій:
use Illuminate\Contracts\Events\ShouldBeDiscovered;
class SendPodcastNotification implements ShouldBeDiscovered
{
/**
* Handle the event.
*/
public function handle(PodcastProcessed $event): void
{
// ...
}
/**
* Determine if the listener should be discovered.
*/
public static function shouldBeDiscovered(): bool
{
return app()->environment('production');
}
}
Реєстрація подій вручну
За допомогою фасаду Event ви можете вручну реєструвати події та відповідних слухачів у методі boot вашого AppServiceProvider:
use App\Domain\Orders\Events\PodcastProcessed;
use App\Domain\Orders\Listeners\SendPodcastNotification;
use Illuminate\Support\Facades\Event;
/**
* Bootstrap any application services.
*/
public function boot(): void
{
Event::listen(
PodcastProcessed::class,
SendPodcastNotification::class,
);
}
Команда event:list дозволяє переглянути всіх слухачів, зареєстрованих у вашому застосунку:
php artisan event:list
Слухачі на замиканнях
Зазвичай слухачів описують класами; проте ви можете вручну зареєструвати й слухачів подій на замиканнях у методі boot вашого AppServiceProvider:
use App\Events\PodcastProcessed;
use Illuminate\Support\Facades\Event;
/**
* Bootstrap any application services.
*/
public function boot(): void
{
Event::listen(function (PodcastProcessed $event) {
// ...
});
}
Анонімні слухачі подій у черзі
Реєструючи слухачів подій на замиканнях, ви можете загорнути замикання слухача у функцію Illuminate\Events\queueable, щоб Laravel виконував слухача через чергу:
use App\Events\PodcastProcessed;
use function Illuminate\Events\queueable;
use Illuminate\Support\Facades\Event;
/**
* Bootstrap any application services.
*/
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()->plus(seconds: 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) {
// The queued listener failed...
}));
Слухачі подій з підстановкою
Ви також можете реєструвати слухачів, використовуючи символ * як підстановочний параметр, - це дозволяє ловити кілька подій одним слухачем. Слухачі з підстановкою першим аргументом отримують ім'я події, а другим - увесь масив даних події:
Event::listen('event.*', function (string $eventName, array $data) {
// ...
});
Опис подій
Клас події - це, по суті, контейнер даних, який містить інформацію, пов'язану з подією. Наприклад, припустімо, що подія 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;
/**
* Create a new event instance.
*/
public function __construct(
public Order $order,
) {}
}
Як бачите, цей клас події не містить логіки. Це контейнер для екземпляра App\Models\Order, який було куплено. Трейт SerializesModels, який використовує подія, коректно серіалізує будь-які моделі Eloquent, якщо об'єкт події серіалізується PHP-функцією serialize, - наприклад, коли ви користуєтеся слухачами в черзі.
Опис слухачів
Далі погляньмо на слухача для нашої прикладної події. Слухачі подій отримують екземпляри подій у своєму методі handle. Команда Artisan make:listener, викликана з опцією --event, автоматично імпортує потрібний клас події й вкаже тип події в методі handle. У методі handle ви можете виконати будь-які дії, потрібні для реакції на подію:
<?php
namespace App\Listeners;
use App\Events\OrderShipped;
class SendShipmentNotification
{
/**
* Create the event listener.
*/
public function __construct() {}
/**
* Handle the event.
*/
public function handle(OrderShipped $event): void
{
// Access the order using $event->order...
}
}
Ваші слухачі подій можуть також вказувати типи потрібних їм залежностей у конструкторах. Усі слухачі подій розв'язуються через сервіс-контейнер Laravel, тож залежності буде впроваджено автоматично.
Припинення поширення події
Інколи вам може захотітися припинити поширення події до інших слухачів. Для цього поверніть false з методу handle вашого слухача.
Слухачі подій у черзі
Ставити слухачів у чергу корисно, якщо слухач виконуватиме повільну задачу - наприклад, надсилатиме лист чи робитиме HTTP-запит. Перш ніж користуватися слухачами в черзі, обов'язково налаштуйте чергу і запустіть воркер черги на сервері чи в локальному середовищі розробки.
Щоб вказати, що слухача слід ставити в чергу, додайте до класу слухача інтерфейс ShouldQueue. Слухачі, згенеровані командами Artisan 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;
use Illuminate\Queue\Attributes\Connection;
use Illuminate\Queue\Attributes\Delay;
use Illuminate\Queue\Attributes\Queue;
#[Connection('sqs')]
#[Queue('listeners')]
#[Delay(60)]
class SendShipmentNotification implements ShouldQueue
{
// ...
}
Якщо ви хочете задати підключення черги, ім'я черги чи затримку слухача під час виконання, опишіть у слухачі методи viaConnection, viaQueue або withDelay:
/**
* Get the name of the listener's queue connection.
*/
public function viaConnection(): string
{
return 'sqs';
}
/**
* Get the name of the listener's queue.
*/
public function viaQueue(): string
{
return 'listeners';
}
/**
* Get the number of seconds before the job should be processed.
*/
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
{
/**
* Reward a gift card to the customer.
*/
public function handle(OrderCreated $event): void
{
// ...
}
/**
* Determine whether the listener should be queued.
*/
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;
/**
* Handle the event.
*/
public function handle(OrderShipped $event): void
{
if ($condition) {
$this->release(30);
}
}
}
Слухачі в черзі та транзакції бази даних
Коли слухачі в черзі диспетчеризуються всередині транзакцій бази даних, черга може обробити їх ще до того, як транзакцію буде зафіксовано. Коли таке трапляється, будь-які зміни, які ви внесли до моделей чи записів у базі під час транзакції, ще можуть не бути в базі. Ба більше, будь-які моделі чи записи, створені всередині транзакції, можуть у базі не існувати. Якщо ваш слухач залежить від цих моделей, під час обробки завдання, яке диспетчеризує слухача в черзі, можуть виникнути несподівані помилки.
Якщо опція конфігурації after_commit вашого підключення черги має значення false, ви все одно можете вказати, що конкретного слухача в черзі слід диспетчеризувати після фіксації всіх відкритих транзакцій бази даних, - реалізуйте в класі слухача інтерфейс ShouldQueueAfterCommit:
<?php
namespace App\Listeners;
use Illuminate\Contracts\Queue\ShouldQueueAfterCommit;
use Illuminate\Queue\InteractsWithQueue;
class SendShipmentNotification implements ShouldQueueAfterCommit
{
use InteractsWithQueue;
}
Щоб дізнатися більше про обхід цих проблем, перегляньте документацію про завдання в черзі та транзакції бази даних.
Middleware слухачів у черзі
Слухачі в черзі можуть також користуватися middleware завдань. Middleware завдань дозволяє огорнути виконання слухачів у черзі власною логікою, зменшивши кількість шаблонного коду в самих слухачах. Створивши middleware завдання, ви можете причепити його до слухача, повернувши з методу middleware слухача:
<?php
namespace App\Listeners;
use App\Events\OrderShipped;
use App\Jobs\Middleware\RateLimited;
use Illuminate\Contracts\Queue\ShouldQueue;
class SendShipmentNotification implements ShouldQueue
{
/**
* Handle the event.
*/
public function handle(OrderShipped $event): void
{
// Process the event...
}
/**
* Get the middleware the listener should pass through.
*
* @return array<int, object>
*/
public function middleware(OrderShipped $event): array
{
return [new RateLimited];
}
}
Зашифровані слухачі в черзі
Laravel дозволяє забезпечити приватність і цілісність даних слухача в черзі за допомогою шифрування. Для початку просто додайте до класу слухача інтерфейс ShouldBeEncrypted. Щойно цей інтерфейс додано до класу, Laravel автоматично зашифрує вашого слухача перед тим, як покласти його в чергу:
<?php
namespace App\Listeners;
use App\Events\OrderShipped;
use Illuminate\Contracts\Queue\ShouldBeEncrypted;
use Illuminate\Contracts\Queue\ShouldQueue;
class SendShipmentNotification implements ShouldQueue, ShouldBeEncrypted
{
// ...
}
Унікальні слухачі подій
Унікальні слухачі потребують драйвера кешу, який підтримує блокування. Наразі атомарні блокування підтримують драйвери кешу
memcached,redis,dynamodb,database,fileтаarray.
Інколи вам може знадобитися гарантувати, що в черзі одночасно перебуває лише один екземпляр конкретного слухача. Для цього реалізуйте в класі слухача інтерфейс ShouldBeUnique:
<?php
namespace App\Listeners;
use App\Events\LicenseSaved;
use Illuminate\Contracts\Queue\ShouldBeUnique;
use Illuminate\Contracts\Queue\ShouldQueue;
class AcquireProductKey implements ShouldQueue, ShouldBeUnique
{
public function __invoke(LicenseSaved $event): void
{
// ...
}
}
У прикладі вище слухач AcquireProductKey є унікальним. Тож слухача не поставлять у чергу, якщо інший його екземпляр уже в черзі й не завершив обробку. Це гарантує, що для кожної ліцензії буде отримано лише один ключ продукту, навіть якщо ліцензію збережено кілька разів поспіль.
У певних випадках вам може знадобитися задати конкретний «ключ», який робить слухача унікальним, або вказати таймаут, після якого слухач перестає бути унікальним. Для цього опишіть у класі слухача властивості або методи uniqueId та uniqueFor. Методи отримують екземпляр події, тож ви можете скористатися даними події, щоб побудувати повернене значення:
<?php
namespace App\Listeners;
use App\Events\LicenseSaved;
use Illuminate\Contracts\Queue\ShouldBeUnique;
use Illuminate\Contracts\Queue\ShouldQueue;
class AcquireProductKey implements ShouldQueue, ShouldBeUnique
{
/**
* The number of seconds after which the listener's unique lock will be released.
*
* @var int
*/
public $uniqueFor = 3600;
public function __invoke(LicenseSaved $event): void
{
// ...
}
/**
* Get the unique ID for the listener.
*/
public function uniqueId(LicenseSaved $event): string
{
return 'listener:'.$event->license->id;
}
}
У прикладі вище слухач AcquireProductKey унікальний за ID ліцензії. Тож будь-які нові диспетчеризації слухача для тієї самої ліцензії ігноруватимуться, доки наявний слухач не завершить обробку. Це не дає отримати дублікати ключів продукту для однієї ліцензії. Крім того, якщо наявного слухача не оброблено протягом години, унікальне блокування буде звільнено, і в чергу можна буде поставити іншого слухача з тим самим унікальним ключем.
Якщо ваш застосунок диспетчеризує події з кількох вебсерверів чи контейнерів, подбайте, щоб усі сервери спілкувалися з одним центральним сервером кешу, - тоді Laravel зможе точно визначити, чи є слухач унікальним.
Тримати слухачів унікальними до початку обробки
За замовчуванням унікальні слухачі «розблоковуються» після того, як слухач завершить обробку або вичерпає всі спроби. Проте бувають ситуації, коли ви хочете, щоб слухач розблоковувався безпосередньо перед обробкою. Для цього ваш слухач має реалізувати контракт ShouldBeUniqueUntilProcessing замість ShouldBeUnique:
<?php
namespace App\Listeners;
use App\Events\LicenseSaved;
use Illuminate\Contracts\Queue\ShouldBeUniqueUntilProcessing;
use Illuminate\Contracts\Queue\ShouldQueue;
class AcquireProductKey implements ShouldQueue, ShouldBeUniqueUntilProcessing
{
// ...
}
Блокування унікальних слухачів
Під капотом, коли диспетчеризується слухач ShouldBeUnique, Laravel намагається отримати блокування з ключем uniqueId. Якщо блокування вже утримується, слухача не диспетчеризують. Це блокування звільняється, коли слухач завершує обробку або вичерпує всі спроби. За замовчуванням Laravel використовує для цього блокування драйвер кешу за замовчуванням. Проте, якщо ви хочете отримувати блокування іншим драйвером, опишіть метод uniqueVia, який поверне потрібний драйвер кешу:
<?php
namespace App\Listeners;
use App\Events\LicenseSaved;
use Illuminate\Contracts\Cache\Repository;
use Illuminate\Support\Facades\Cache;
class AcquireProductKey implements ShouldQueue, ShouldBeUnique
{
// ...
/**
* Get the cache driver for the unique listener lock.
*/
public function uniqueVia(LicenseSaved $event): Repository
{
return Cache::driver('redis');
}
}
Якщо вам потрібно лише обмежити одночасну обробку слухача, скористайтеся натомість middleware завдання WithoutOverlapping.
Обробка невдалих завдань
Інколи ваші слухачі подій у черзі можуть зазнавати невдачі. Якщо слухач у черзі перевищить максимальну кількість спроб, задану вашим воркером черги, у слухачі буде викликано метод 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;
/**
* Handle the event.
*/
public function handle(OrderShipped $event): void
{
// ...
}
/**
* Handle a job failure.
*/
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\Attributes\Tries;
use Illuminate\Queue\InteractsWithQueue;
#[Tries(5)]
class SendShipmentNotification implements ShouldQueue
{
use InteractsWithQueue;
// ...
}
Як альтернативу заданню кількості спроб перед невдачею ви можете вказати момент, після якого спроби виконати слухача припиняються. Це дозволяє виконувати слухача будь-яку кількість разів у межах заданого проміжку часу. Щоб задати момент, після якого слухача більше не намагатимуться виконати, додайте до класу слухача метод retryUntil. Цей метод має повернути екземпляр DateTimeInterface:
use DateTimeInterface;
/**
* Determine the time at which the listener should timeout.
*/
public function retryUntil(): DateTimeInterface
{
return now()->plus(minutes: 5);
}
Якщо описано і retryUntil, і tries, Laravel віддає перевагу методу retryUntil.
Задання відступу для слухача в черзі
Якщо ви хочете налаштувати, скільки секунд Laravel має чекати перед повторною спробою виконати слухача, який натрапив на виняток, скористайтеся атрибутом Backoff у класі слухача:
<?php
namespace App\Listeners;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Queue\Attributes\Backoff;
#[Backoff(3)]
class SendShipmentNotification implements ShouldQueue
{
// ...
}
Якщо для визначення часу відступу слухача вам потрібна складніша логіка, опишіть у класі слухача метод backoff:
/**
* Calculate the number of seconds to wait before retrying the queued listener.
*/
public function backoff(OrderShipped $event): int
{
return 3;
}
Ви легко можете налаштувати «експоненційні» відступи, повернувши з методу backoff масив значень. У цьому прикладі затримка перед повтором становитиме 1 секунду для першого повтору, 5 секунд для другого, 10 секунд для третього і 10 секунд для кожного наступного, якщо спроби ще лишилися:
/**
* Calculate the number of seconds to wait before retrying the queued listener.
*
* @return list<int>
*/
public function backoff(OrderShipped $event): array
{
return [1, 5, 10];
}
Задання максимальної кількості винятків для слухача в черзі
Інколи вам може знадобитися вказати, що слухача в черзі можна намагатися виконати багато разів, але він має зазнати невдачі, якщо повтори спричинено заданою кількістю необроблених винятків (на відміну від звільнення методом release напряму). Для цього скористайтеся атрибутами Tries і MaxExceptions у класі слухача:
<?php
namespace App\Listeners;
use App\Events\OrderShipped;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Queue\Attributes\MaxExceptions;
use Illuminate\Queue\Attributes\Tries;
use Illuminate\Queue\InteractsWithQueue;
#[Tries(25)]
#[MaxExceptions(3)]
class SendShipmentNotification implements ShouldQueue
{
use InteractsWithQueue;
/**
* Handle the event.
*/
public function handle(OrderShipped $event): void
{
// Process the event...
}
}
У цьому прикладі слухача буде повторено до 25 разів. Проте слухач зазнає невдачі, якщо викине три необроблені винятки.
Задання таймауту для слухача в черзі
Часто ви приблизно знаєте, скільки часу мають виконуватися ваші слухачі в черзі. Тому Laravel дозволяє задати значення «таймауту». Якщо слухач обробляється довше за вказану кількість секунд, воркер, який його обробляє, завершиться з помилкою. Задати максимальну кількість секунд, яку дозволено виконуватися слухачеві, можна атрибутом Timeout у класі слухача:
<?php
namespace App\Listeners;
use App\Events\OrderShipped;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Queue\Attributes\Timeout;
#[Timeout(120)]
class SendShipmentNotification implements ShouldQueue
{
// ...
}
Якщо ви хочете, щоб слухача позначало як невдалого після таймауту, скористайтеся атрибутом FailOnTimeout у класі слухача:
<?php
namespace App\Listeners;
use App\Events\OrderShipped;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Queue\Attributes\FailOnTimeout;
#[FailOnTimeout]
class SendShipmentNotification implements ShouldQueue
{
// ...
}
Диспетчеризація подій
Щоб диспетчеризувати подію, викличте на ній статичний метод dispatch. Цей метод стає доступним у події завдяки трейту Illuminate\Foundation\Events\Dispatchable. Будь-які аргументи, передані методу dispatch, буде передано в конструктор події:
<?php
namespace App\Http\Controllers;
use App\Events\OrderShipped;
use App\Models\Order;
use Illuminate\Http\RedirectResponse;
use Illuminate\Http\Request;
class OrderShipmentController extends Controller
{
/**
* Ship the given order.
*/
public function store(Request $request): RedirectResponse
{
$order = Order::findOrFail($request->order_id);
// Order shipment logic...
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;
/**
* Create a new event instance.
*/
public function __construct(
public Order $order,
) {}
}
Відкладення подій
Відкладені події дозволяють затримати диспетчеризацію подій моделей і виконання слухачів подій до завершення певного блоку коду. Це особливо корисно, коли вам потрібно переконатися, що всі пов'язані записи створено, перш ніж спрацюють слухачі подій.
Щоб відкласти події, передайте замикання методу Event::defer():
use App\Models\User;
use Illuminate\Support\Facades\Event;
Event::defer(function () {
$user = User::create(['name' => 'Victoria Otwell']);
$user->posts()->create(['title' => 'My first post!']);
});
Усі події, спричинені всередині замикання, буде диспетчеризовано після його виконання. Це гарантує, що слухачі подій матимуть доступ до всіх пов'язаних записів, створених під час відкладеного виконання. Якщо всередині замикання станеться виняток, відкладені події не буде диспетчеризовано.
Щоб відкласти лише певні події, передайте масив подій другим аргументом методу defer:
use App\Models\User;
use Illuminate\Support\Facades\Event;
Event::defer(function () {
$user = User::create(['name' => 'Victoria Otwell']);
$user->posts()->create(['title' => 'My first post!']);
}, ['eloquent.created: '.User::class]);
Підписники подій
Написання підписників подій
Підписники подій - це класи, які можуть підписуватися на кілька подій зсередини самого класу підписника, дозволяючи описати кілька обробників подій в одному класі. Підписники мають описувати метод subscribe, який отримує екземпляр диспетчера подій. Щоб зареєструвати слухачів подій, викличте на переданому диспетчері метод listen:
<?php
namespace App\Listeners;
use Illuminate\Auth\Events\Login;
use Illuminate\Auth\Events\Logout;
use Illuminate\Events\Dispatcher;
class UserEventSubscriber
{
/**
* Handle user login events.
*/
public function handleUserLogin(Login $event): void {}
/**
* Handle user logout events.
*/
public function handleUserLogout(Logout $event): void {}
/**
* Register the listeners for the subscriber.
*/
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
{
/**
* Handle user login events.
*/
public function handleUserLogin(Login $event): void {}
/**
* Handle user logout events.
*/
public function handleUserLogout(Logout $event): void {}
/**
* Register the listeners for the subscriber.
*
* @return array<string, string>
*/
public function subscribe(Dispatcher $events): array
{
return [
Login::class => 'handleUserLogin',
Logout::class => 'handleUserLogout',
];
}
}
Реєстрація підписників подій
Коли підписника написано, Laravel автоматично зареєструє його методи-обробники, якщо ті відповідають домовленостям виявлення подій Laravel. Інакше ви можете зареєструвати підписника вручну методом subscribe фасаду Event. Зазвичай це роблять у методі boot вашого AppServiceProvider:
<?php
namespace App\Providers;
use App\Listeners\UserEventSubscriber;
use Illuminate\Support\Facades\Event;
use Illuminate\Support\ServiceProvider;
class AppServiceProvider extends ServiceProvider
{
/**
* Bootstrap any application services.
*/
public function boot(): void
{
Event::subscribe(UserEventSubscriber::class);
}
}
Тестування
Тестуючи код, який диспетчеризує події, ви можете захотіти сказати Laravel не виконувати слухачів події, адже код слухача можна протестувати напряму й окремо від коду, який диспетчеризує відповідну подію. Звісно, щоб протестувати самого слухача, ви можете створити його екземпляр і викликати метод handle напряму у вашому тесті.
За допомогою методу fake фасаду Event ви можете завадити виконанню слухачів, виконати тестований код, а потім перевірити, які події диспетчеризував ваш застосунок, методами assertDispatched, assertNotDispatched та assertNothingDispatched:
<?php
use App\Events\OrderFailedToShip;
use App\Events\OrderShipped;
use Illuminate\Support\Facades\Event;
test('orders can be shipped', function () {
Event::fake();
// Perform order shipping...
// Assert that an event was dispatched...
Event::assertDispatched(OrderShipped::class);
// Assert an event was dispatched twice...
Event::assertDispatched(OrderShipped::class, 2);
// Assert an event was dispatched once...
Event::assertDispatchedOnce(OrderShipped::class);
// Assert an event was not dispatched...
Event::assertNotDispatched(OrderFailedToShip::class);
// Assert that no events were dispatched...
Event::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
{
/**
* Test order shipping.
*/
public function test_orders_can_be_shipped(): void
{
Event::fake();
// Perform order shipping...
// Assert that an event was dispatched...
Event::assertDispatched(OrderShipped::class);
// Assert an event was dispatched twice...
Event::assertDispatched(OrderShipped::class, 2);
// Assert an event was dispatched once...
Event::assertDispatchedOnce(OrderShipped::class);
// Assert an event was not dispatched...
Event::assertNotDispatched(OrderFailedToShip::class);
// Assert that no events were dispatched...
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:
test('orders can be processed', function () {
Event::fake([
OrderCreated::class,
]);
$order = Order::factory()->create();
Event::assertDispatched(OrderCreated::class);
// Other events are dispatched as normal...
$order->update([
// ...
]);
});
/**
* Test order process.
*/
public function test_orders_can_be_processed(): void
{
Event::fake([
OrderCreated::class,
]);
$order = Order::factory()->create();
Event::assertDispatched(OrderCreated::class);
// Other events are dispatched as normal...
$order->update([
// ...
]);
}
Ви можете підмінити всі події, окрім заданого набору, методом except:
Event::fake()->except([
OrderCreated::class,
]);
Обмежені підміни подій
Якщо ви хочете підмінити слухачів подій лише для частини вашого тесту, скористайтеся методом fakeFor:
<?php
use App\Events\OrderCreated;
use App\Models\Order;
use Illuminate\Support\Facades\Event;
test('orders can be processed', function () {
$order = Event::fakeFor(function () {
$order = Order::factory()->create();
Event::assertDispatched(OrderCreated::class);
return $order;
});
// Events are dispatched as normal and observers will run...
$order->update([
// ...
]);
});
<?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;
});
// Events are dispatched as normal and observers will run...
$order->update([
// ...
]);
}
}