Event — это факт, что что-то произошло. Например: «пользователь зарегистрировался», «заказ создан», «платёж прошёл».
Listener — это реакция на этот факт. Что нужно сделать, когда событие произошло.
Проблема: всё в одном сервисе
Без Event/Listener побочные эффекты накапливаются прямо в сервисе. Создание заказа — одно действие, а email, Slack, бонусы, статистика — побочные эффекты, которые к нему налипли.
class OrderService
{
public function create(CreateOrderDTO $dto): Order
{
$order = Order::create([...]);
// Побочные эффекты растут с каждой новой задачей
Mail::send(...);
Slack::notify(...);
$user->addBonusPoints(...);
Stats::increment(...);
return $order;
}
}
С каждым новым требованием метод разрастается. Сервис начинает знать обо всём — email, Slack, бонусы — хотя его задача только создать заказ.
Решение: Event + Listener
Сервис просто говорит «заказ создан» — и больше ни о чём не думает:
class OrderService
{
public function create(CreateOrderDTO $dto): Order
{
$order = Order::create([...]);
// Одна строка вместо четырёх блоков
event(new OrderCreated($order));
return $order;
}
}
Event — контейнер с данными
Event — это простой класс, который хранит данные о том, что произошло. Никакой логики — только свойства. Создаётся командой php artisan make:event OrderCreated.
// app/Events/OrderCreated.php
namespace App\Events;
use App\Models\Order;
class OrderCreated
{
public function __construct(
public readonly Order $order,
) {}
}
Listener — реакция на событие
Каждая реакция — отдельный класс с методом handle(). Listener получает Event и делает свою работу. Создаётся командой php artisan make:listener SendOrderEmail.
// app/Listeners/SendOrderEmail.php
namespace App\Listeners;
use App\Events\OrderCreated;
use App\Mail\OrderConfirmation;
use Illuminate\Support\Facades\Mail;
class SendOrderEmail
{
public function handle(OrderCreated $event): void
{
Mail::to($event->order->user->email)->send(
new OrderConfirmation($event->order)
);
}
}
// app/Listeners/NotifyManagerInSlack.php
namespace App\Listeners;
use App\Events\OrderCreated;
use Illuminate\Support\Facades\Slack;
class NotifyManagerInSlack
{
public function handle(OrderCreated $event): void
{
Slack::channel('orders')->send(
"Новый заказ #{$event->order->id}"
);
}
}
// app/Listeners/AddBonusPoints.php
namespace App\Listeners;
use App\Events\OrderCreated;
class AddBonusPoints
{
public function handle(OrderCreated $event): void
{
$user = $event->order->user;
$points = (int) ($event->order->total_price * 0.1);
$user->increment('bonus_points', $points);
}
}
Регистрация: связь Event → Listener
Laravel должен знать, какие Listener-ы реагируют на какие Event-ы. Это указывается в AppServiceProvider:
// app/Providers/AppServiceProvider.php
namespace App\Providers;
use App\Events\OrderCreated;
use App\Listeners\SendOrderEmail;
use App\Listeners\NotifyManagerInSlack;
use App\Listeners\AddBonusPoints;
use Illuminate\Support\Facades\Event;
use Illuminate\Support\ServiceProvider;
class AppServiceProvider extends ServiceProvider
{
public function boot(): void
{
Event::listen(OrderCreated::class, SendOrderEmail::class);
Event::listen(OrderCreated::class, NotifyManagerInSlack::class);
Event::listen(OrderCreated::class, AddBonusPoints::class);
}
}
Теперь один вызов event(new OrderCreated($order)) запускает три Listener-а. Нужна новая реакция — создаёшь ещё один Listener и регистрируешь. Сервис вообще не трогаешь.
Структура файлов
app/
├── Events/
│ └── OrderCreated.php ← что произошло
├── Listeners/
│ ├── SendOrderEmail.php ← реакция: email клиенту
│ ├── NotifyManagerInSlack.php ← реакция: сообщение в Slack
│ └── AddBonusPoints.php ← реакция: начислить бонусы
├── Services/
│ └── OrderService.php ← вызывает event()
└── Providers/
└── AppServiceProvider.php ← связывает Event → Listener
Аналогия с WordPress
По сути это тот же принцип, что хуки в WordPress — только типизированный и с классами:
// WordPress
do_action('order_created', $order);
add_action('order_created', 'send_order_email');
add_action('order_created', 'notify_slack');
// Laravel
event(new OrderCreated($order));
Event::listen(OrderCreated::class, SendOrderEmail::class);
Event::listen(OrderCreated::class, NotifyManagerInSlack::class);
Разница: в WordPress хук — это строка 'order_created', легко опечататься. В Laravel Event — это класс с тайп-хинтом, IDE подскажет и подчеркнёт ошибку.
Асинхронные Listener-ы (очереди)
По умолчанию Listener-ы выполняются синхронно — пользователь ждёт, пока отправится email и сообщение в Slack. Чтобы выполнить реакцию в фоне, достаточно добавить интерфейс ShouldQueue:
use Illuminate\Contracts\Queue\ShouldQueue;
class SendOrderEmail implements ShouldQueue
{
public function handle(OrderCreated $event): void
{
// Этот код выполнится в фоне, через очередь
Mail::to($event->order->user->email)->send(
new OrderConfirmation($event->order)
);
}
}
Одна строка — implements ShouldQueue — и Listener уходит в очередь. Пользователь получает ответ мгновенно, а email отправляется в фоне.