Большинство приложений на Laravel растут по одному сценарию. Контроллер начинает делать слишком много: создавать пользователя, отправлять приветственное письмо, записывать аналитику, отправлять уведомление в Slack — и все это в одном методе. Система событий в Laravel создана как раз для того, чтобы разделить эти задачи.

События в Laravel: Полное руководство + Что нового в 2026 году

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

Короткий ответ

События в Laravel позволяют одной части вашего приложения объявить о том, что что-то произошло (UserRegistered, OrderShipped), не зная и не заботясь о том, кто на это реагирует. Слушатели (listeners) подписываются на эти события и реагируют независимо — отправляют письма, обновляют поисковый индекс, вызывают вебхуки. Это позволяет отделить основную бизнес-логику от побочных эффектов.

Проблема, которую действительно решают события

Вот как выглядит паттерн без использования событий:

public function store(Request $request)
{
    $user = User::create($request->validated());    Mail::to($user)->send(new WelcomeEmail($user));
    Analytics::track('user_registered', $user);
    Slack::notify("New signup: {$user->email}");    return redirect('/dashboard');
}

Каждый раз, когда вы добавляете новый побочный эффект, этот метод разрастается. Тестирование такого метода требует создания моков для четырех несвязанных систем только для того, чтобы проверить, что пользователь был создан. И если вам понадобится создавать пользователя в двух разных местах приложения, вам придется дублировать весь этот код.

С событиями контроллер делает только одну вещь:

public function store(Request $request)
{
    $user = User::create($request->validated());    event(new UserRegistered($user));    return redirect('/dashboard');
}

Все остальное — отправка письма, вызов аналитики, уведомление в Slack — становится независимыми слушателями, которые реагируют на UserRegistered, при этом контроллер даже не подозревает об их существовании.

Создание событий и слушателей

php artisan make:event UserRegistered
php artisan make:listener SendWelcomeEmail --event=UserRegistered

Событие — это просто контейнер для данных:

class UserRegistered
{
    public function __construct(public User $user) {}
}

Слушатель выполняет непосредственную работу:

class SendWelcomeEmail
{
    public function handle(UserRegistered $event): void
    {
        Mail::to($event->user)->send(new WelcomeEmail($event->user));
    }
}

Laravel автоматически обнаруживает слушателей, сканируя директорию Listeners и сопоставляя тип события, указанный в аргументе метода handle() — в обычном приложении ручная регистрация не требуется.

Очереди для слушателей (не заставляйте пользователей ждать)

Все, что работает медленно — отправка писем, запросы к сторонним API, генерация отчетов — не должно блокировать запрос. Сделайте слушатель асинхронным с помощью одного интерфейса:

class SendWelcomeEmail implements ShouldQueue
{
    public function handle(UserRegistered $event): void
    {
        Mail::to($event->user)->send(new WelcomeEmail($event->user));
    }
}

Это единственное изменение. Контроллер по-прежнему просто вызывает event() — Laravel сам позаботится о том, чтобы отправить слушатель в очередь, а не выполнять его синхронно.

Что нового: Дебаунсинг слушателей в очереди (Laravel 13.26)

Это действительно новое и полезное дополнение, которое решает реальную проблему в продакшене, с которой рано или поздно сталкиваются большинство приложений.

Представьте импорт товаров, который обновляет одну и ту же запись 40 раз за минуту. Событие ProductUpdated срабатывает 40 раз, и слушатель, который перестраивает поисковый индекс, тоже запускается 40 раз — при этом каждый запуск индексирует состояние, которое следующий запуск тут же перезаписывает. Это почти 97% бесполезной работы.

В Laravel 13.26 для решения этой проблемы был добавлен атрибут #[DebounceFor]:

use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Queue\Attributes\DebounceFor;#[DebounceFor(30, maxWait: 120)]
class UpdateProductSearchIndex implements ShouldQueue
{
    public function debounceId(ProductUpdated $event): string
    {
        return (string) $event->product->getKey();
    }    public function handle(ProductUpdated $event): void
    {
        ProductIndexer::index($event->product->fresh());
    }
}

Вот как это работает на практике: при каждой отправке события слушатель помещается в очередь с задержкой в 30 секунд, а в кэше сохраняется токен владения. Более поздняя отправка перезаписывает этот токен. Когда старая, уже находящаяся в очереди копия наконец запускается, она проверяет токен, видит, что больше не владеет им, и тихо завершает работу, не выполняя лишних действий.

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

Это та функция, которую начинаешь ценить только тогда, когда сам сталкиваешься с подобной проблемой — когда лавина частых событий перегружает слушатель, которому достаточно было запуститься всего один раз.

Что нового: Управление обнаружением слушателей (Laravel 13.12)

Автоматическое обнаружение (auto-discovery) — это удобно, пока у вас не появится слушатель, который не должен запускаться в определенных окружениях. Например, синхронизация с CRM, которая нужна только на продакшене, а на локальной машине будет выдавать ошибки из-за отсутствия настроенных доступов.

В Laravel 13.12 появился интерфейс ShouldBeDiscovered, который позволяет слушателю самостоятельно определять, нужно ли его обнаруживать:

use Illuminate\Contracts\Events\ShouldBeDiscovered;
use Illuminate\Contracts\Queue\ShouldQueue;class NotifyExternalCrm implements ShouldBeDiscovered, ShouldQueue
{
    public static function shouldBeDiscovered(): bool
    {
        return app()->environment('production');
    }    public function handle(CustomerRegistered $event): void
    {
        // sync to CRM
    }
}

Раньше, чтобы исключить специфичный для окружения слушатель из автообнаружения, приходилось полностью отказываться от него и регистрировать все слушатели вручную — компромисс по принципу «все или ничего». Теперь это решение принимается с помощью одного метода прямо в классе слушателя.

События против задач (Jobs): Вопрос, который стоит прояснить на берегу

Их часто путают, хотя разница между ними принципиальна для архитектуры приложения.

EventsJobsНазначениеОбъявить, что что-то произошлоВыполнить конкретную единицу работыСлушателиНоль, один или много могут отреагироватьОдна задача делает одну вещьСвязанностьОтправитель не знает, кто слушаетВызывающий код явно запускает задачуТипичный пример«Пользователь зарегистрировался» → несколько реакций«Отправить конкретное письмо»

Хорошее эмпирическое правило: если в ответ на одно событие должно произойти несколько несвязанных действий, используйте событие с несколькими слушателями. Если вы запускаете одну конкретную, целенаправленную задачу, то это просто Job — отправляйте её напрямую.

Тестирование событий без запуска реальных побочных эффектов

Метод Event::fake() в Laravel позволяет убедиться, что событие было отправлено, без фактического запуска каждого слушателя — это одна из самых удобных возможностей для тестирования в Laravel:

public function test_registering_fires_user_registered_event()
{
    Event::fake();    $this->post('/register', [...]);    Event::assertDispatched(UserRegistered::class, function ($event) {
        return $event->user->email === 'test@example.com';
    });
}

Это проверяет, что событие сработало с правильными данными, избавляя от необходимости создавать моки для Mail, Slack и аналитики по отдельности только для того, чтобы протестировать процесс регистрации.

Частые ошибки при работе с событиями в Laravel

  •  Размещение бизнес-логики внутри слушателей, когда ей место в сервисном классе. Слушатели предназначены для реагирования на события, а не для хранения сложной логики. Иначе эту логику будет трудно переиспользовать вне цепочки событий.
  •  Забытый интерфейс  ShouldQueue на медленных задачах. Слушатель, выполняющий внешний API-запрос синхронно, незаметно превращает быстрый эндпоинт в медленный, пока кто-то не обратит внимание на задержки.
  •  Избыточное использование событий для простых одиночных действий. Не все должно быть событием. Для одной конкретной задачи обычно лучше использовать Job или простой вызов метода, а не событие с единственным слушателем.
  •  Отсутствие тестов на отправку событий. Легко провести рефакторинг контроллера и случайно удалить вызов event(). Проверки с помощью Event::fake() помогают поймать это до деплоя.

В заключение

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

Новые возможности — дебаунсинг слушателей для защиты от перегрузок и управление автообнаружением для разных окружений — решают реальные проблемы, возникающие при наличии настоящего трафика на продакшене. О них полезно знать, даже если они не понадобятся вам в первый же день.

FAQ

 В чем разница между событиями и задачами (jobs) в Laravel? События объявляют о том, что что-то произошло, и могут запускать ноль, одного или много слушателей, при этом отправитель не знает о них. Задачи — это единичные, явно запускаемые блоки работы.

 Как запустить слушатель Laravel в фоновом режиме? Добавьте интерфейс ShouldQueue к классу слушателя — никаких других изменений в коде не требуется, Laravel автоматически отправит его в очередь вместо синхронного выполнения.

 Что такое  #[DebounceFor] в Laravel? Это атрибут, появившийся в Laravel 13.26 для слушателей в очереди. Он объединяет частые повторяющиеся события в один запуск, предотвращая дублирование работы, когда одно и то же событие срабатывает много раз подряд за короткое время.

 Как протестировать, что событие Laravel было отправлено? Используйте Event::fake() перед тестируемым действием, а затем сделайте утверждение с помощью Event::assertDispatched(EventClass::class) — это проверит факт отправки события без запуска его слушателей.

 Можно ли исключить слушатель Laravel из автообнаружения в определенных окружениях? Да — для этого реализуйте интерфейс ShouldBeDiscovered и верните false из статического метода shouldBeDiscovered() на основе значения app()->environment() (функция добавлена в Laravel 13.12).