Документация Laravel 10.x
Здесь ты найдешь сниппеты по Laravel и полезные советы по веб-разработке.
События 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([...]); }}