Поддержите проект сделав пожертвование.
Будьте в курсе последних новостей!
Будьте в курсе последних новостей!

События (Events)

Введение

События 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

For convenience, you may also invoke the make:event and make:listener Artisan commands without additional arguments. When you do so, Laravel will automatically prompt you for the class name and, when creating a listener, the event it should listen to: Для удобства вы также можете вызывать команды Artisan make:event и make:listener без дополнительных аргументов. Когда вы это сделаете, Laravel автоматически предложит вам ввести имя класса и, при создании слушателя, событие, которое он должен прослушивать:

php artisan make:event

php artisan make:listener

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

Автопоиск событий

По умолчанию Laravel автоматически найдет и зарегистрирует ваших слушателей событий, просканировав каталог Listeners вашего приложения. Когда Laravel находит какой-либо метод класса слушателя, который начинается с handle или __invoke, Laravel регистрирует эти методы как слушатели событий для события, тип которого указан в сигнатуре метода:

use App\Events\PodcastProcessed;

class SendPodcastNotification
{
    /**
     * Handle the given event.
     */
    public function handle(PodcastProcessed $event): void
    {
        // ...
    }
}

Если вы планируете хранить свои слушатели в другом каталоге или в нескольких каталогах, вы можете поручить Laravel сканировать эти каталоги с помощью метода withEvents в файле bootstrap/app.php вашего приложения:

->withEvents(discover: [
    __DIR__.'/../app/Domain/Orders/Listeners',
])

Команда event:list может использоваться для вывода списка всех слушателей, зарегистрированных в вашем приложении:

php artisan event:list

Кэширование событий

Чтобы повысить скорость вашего приложения, вам следует кэшировать манифест всех прослушивателей вашего приложения с помощью Artisan-команд optimize или event:cache. Обычно эту команду следует запускать как часть процесса развертывания. Этот манифест будет использоваться платформой для ускорения процесса регистрации событий. Команда event:clear может использоваться для уничтожения кэша событий.

Ручная регистрация событий

Используя фасад Event, вы можете вручную регистрировать события и соответствующие им слушателей в методе boot AppServiceProvider вашего приложения:

use App\Domain\Orders\Events\PodcastProcessed;
use App\Domain\Orders\Listeners\SendPodcastNotification;
use Illuminate\Support\Facades\Event;

/**
 * Запуск любых служб приложения.
 */
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;

/**
 * Запуск любых служб приложения.
 */
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) {
    // ...
});

Определение событий

Класс событий – это, по сути, контейнер данных, который содержит информацию, относящуюся к событию. Например, предположим, что событие 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 make:listener при вызове с опцией --event автоматически импортирует соответствующий класс события и указывает тип события в методе 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 в класс слушателя. Слушатели, сгенерированные командами event:generate и make:listener Artisan, уже будут иметь этот интерфейс, импортируемый в текущее пространство имен, поэтому вы можете использовать его немедленно:

<?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, то вы все равно можете указать, что конкретный слушатель в очереди должен быть выполнен после того, как все открытые транзакции в базе данных будут завершены, реализовав интерфейс ShouldQueueAfterCommit в классе слушателя:

<?php

namespace App\Listeners;

use Illuminate\Contracts\Queue\ShouldQueueAfterCommit;
use Illuminate\Queue\InteractsWithQueue;

class SendShipmentNotification implements ShouldQueueAfterCommit
{
    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;

/**
 * Определить время, через которое слушатель должен отключиться.
 *
 * @return \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;

    /**
     * Create a new event instance.
     */
    public function __construct(
        public Order $order,
    ) {}
}

Подписчики событий

Написание подписчиков на события

Подписчики событий – это классы, которые могут подписываться на несколько событий, что позволяет вам определять несколько обработчиков событий в одном классе. Подписчики должны определить метод subscribe, которому будет передан экземпляр диспетчера событий. Вы можете вызвать метод listen данного диспетчера для регистрации слушателей событий:

<?php

namespace App\Listeners;

use Illuminate\Auth\Events\Login;
use Illuminate\Auth\Events\Logout;

class UserEventSubscriber
{
    /**
     * Обработать событие входа пользователя в систему.
     */
    public function handleUserLogin(Login $event): void {}

    /**
     * Обработать событие выхода пользователя из системы.
     */
    public function handleUserLogout(Logout $event): void {}

    /**
     * Зарегистрировать слушателей для подписчика.
     *
     * @param  \Illuminate\Events\Dispatcher  $events
     * @return 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 {}

    /**
     * Register the listeners for the subscriber.
     */
    public function subscribe(Dispatcher $events): array
    {
        return [
            Login::class => 'handleUserLogin',
            Logout::class => 'handleUserLogout',
        ];
    }
}

Регистрация подписчиков на события

После написания подписчика вы готовы зарегистрировать его в диспетчере событий. Вы можете зарегистрировать подписчиков, используя метод 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
{
    /**
     * Загрузка любых сервисов приложения.
     */
    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();

    // Выполните процесс доставки заказа...

    // Утвердите, что событие было отправлено...
    Event::assertDispatched(OrderShipped::class);

    // Утвердите, что событие было отправлено дважды...
    Event::assertDispatched(OrderShipped::class, 2);

    // Утвердите, что событие не было отправлено...
    Event::assertNotDispatched(OrderFailedToShip::class);

    // Утвердите, что не было отправлено ни одного события...
    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();

        // Выполните процесс доставки заказа...

        // Утвердите, что событие было отправлено...
        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:

test('orders can be processed', function () {
    Event::fake([
        OrderCreated::class,
    ]);

    $order = Order::factory()->create();

    Event::assertDispatched(OrderCreated::class);

    // Другие события отправляются как обычно...
    $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);

    // Другие события отправляются как обычно...
    $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;
        });

        // События отправляются как обычно, и наблюдатели будут выполнены...
        $order->update([...]);
    }
}