Глава 10.4: Events и Listeners — событийная архитектура, Observers, подписчики
📋 Содержание главы
- Что такое событийная архитектура и зачем она нужна
- Events и Listeners в Laravel
- Создание и регистрация событий
- Observers — удобная альтернатива для моделей
- Event Subscribers — группировка логики
- Практические примеры и лучшие практики
1. Что такое событийная архитектура
Проблема связанного кода
Представь, что при регистрации пользователя нужно:
- Отправить welcome email
- Создать профиль
- Начислить бонусы
- Уведомить администратора
- Записать в аналитику
Плохой подход (всё в контроллере):
class RegisterController extends Controller
{
public function register(Request $request)
{
$user = User::create($request->validated());
// Отправка email
Mail::to($user)->send(new WelcomeEmail($user));
// Создание профиля
Profile::create(['user_id' => $user->id]);
// Начисление бонусов
Bonus::create([
'user_id' => $user->id,
'amount' => 100
]);
// Уведомление админа
$admin = User::where('role', 'admin')->first();
Notification::send($admin, new NewUserNotification($user));
// Аналитика
Analytics::track('user_registered', $user->id);
return redirect('/dashboard');
}
}Проблемы:
- Контроллер перегружен логикой
- Сложно тестировать
- Нельзя переиспользовать логику
- Трудно добавлять новые действия
- Всё выполняется синхронно (медленно)
Событийная архитектура — решение
Events (События) — это сигналы о том, что что-то произошло в системе.
Listeners (Слушатели) — обработчики, которые реагируют на события.
// Контроллер — только создание пользователя
$user = User::create($request->validated());
// Генерируем событие
event(new UserRegistered($user));
// Всё остальное произойдёт автоматически!Преимущества:
- ✅ Разделение ответственности
- ✅ Легко добавлять новые действия
- ✅ Можно выполнять асинхронно
- ✅ Легче тестировать
- ✅ Переиспользование логики
2. Events и Listeners в Laravel
Создание Event
php artisan make:event UserRegistered// app/Events/UserRegistered.php
namespace App\Events;
use App\Models\User;
use Illuminate\Foundation\Events\Dispatchable;
use Illuminate\Queue\SerializesModels;
class UserRegistered
{
use Dispatchable, SerializesModels;
public function __construct(
public User $user
) {}
}Трейты:
Dispatchable— позволяет вызыватьevent(new UserRegistered($user))SerializesModels— сериализует модели для очередей
Создание Listener
php artisan make:listener SendWelcomeEmail --event=UserRegistered// app/Listeners/SendWelcomeEmail.php
namespace App\Listeners;
use App\Events\UserRegistered;
use App\Mail\WelcomeEmail;
use Illuminate\Support\Facades\Mail;
class SendWelcomeEmail
{
public function handle(UserRegistered $event): void
{
Mail::to($event->user)->send(
new WelcomeEmail($event->user)
);
}
}Регистрация в EventServiceProvider
// app/Providers/EventServiceProvider.php
namespace App\Providers;
use App\Events\UserRegistered;
use App\Listeners\SendWelcomeEmail;
use App\Listeners\CreateUserProfile;
use App\Listeners\GiveBonusPoints;
use Illuminate\Foundation\Support\Providers\EventServiceProvider as ServiceProvider;
class EventServiceProvider extends ServiceProvider
{
protected $listen = [
UserRegistered::class => [
SendWelcomeEmail::class,
CreateUserProfile::class,
GiveBonusPoints::class,
],
];
}Одно событие → много слушателей!
Запуск события
// Вариант 1: Функция-хелпер
event(new UserRegistered($user));
// Вариант 2: Фасад
Event::dispatch(new UserRegistered($user));
// Вариант 3: Метод события
UserRegistered::dispatch($user);3. Асинхронная обработка событий
Listener в очереди
namespace App\Listeners;
use Illuminate\Contracts\Queue\ShouldQueue;
class SendWelcomeEmail implements ShouldQueue
{
public $queue = 'emails'; // Имя очереди
public $delay = 10; // Задержка в секундах
public $tries = 3; // Количество попыток
public function handle(UserRegistered $event): void
{
Mail::to($event->user)->send(
new WelcomeEmail($event->user)
);
}
// Что делать при ошибке
public function failed(UserRegistered $event, \Throwable $exception): void
{
Log::error("Failed to send welcome email: {$exception->getMessage()}");
}
}Важно: Event должен быть сериализуемым!
Условная обработка
class SendWelcomeEmail implements ShouldQueue
{
public function handle(UserRegistered $event): void
{
Mail::to($event->user)->send(
new WelcomeEmail($event->user)
);
}
// Выполнить только если условие истинно
public function shouldQueue(UserRegistered $event): bool
{
return $event->user->email_verified_at !== null;
}
}4. Model Observers — события моделей
Проблема
У Eloquent есть встроенные события:
creating,createdupdating,updatedsaving,saveddeleting,deletedrestoring,restored
Можно слушать их так:
// В EventServiceProvider
protected $listen = [
'eloquent.created: App\Models\User' => [
SendWelcomeEmail::class,
],
];Но это неудобно для моделей. Для них есть Observers.
Создание Observer
php artisan make:observer UserObserver --model=User// app/Observers/UserObserver.php
namespace App\Observers;
use App\Models\User;
use Illuminate\Support\Str;
class UserObserver
{
// Перед созданием
public function creating(User $user): void
{
if (empty($user->uuid)) {
$user->uuid = Str::uuid();
}
}
// После создания
public function created(User $user): void
{
// Создаём профиль
$user->profile()->create([
'bio' => 'Новый пользователь'
]);
// Начисляем бонусы
$user->bonuses()->create([
'amount' => 100,
'description' => 'Приветственный бонус'
]);
}
// Перед обновлением
public function updating(User $user): void
{
if ($user->isDirty('email')) {
$user->email_verified_at = null;
}
}
// После обновления
public function updated(User $user): void
{
if ($user->wasChanged('email')) {
Mail::to($user)->send(new EmailChangedNotification());
}
}
// Перед удалением
public function deleting(User $user): void
{
// Удаляем связанные данные
$user->posts()->delete();
$user->comments()->delete();
}
// После удаления
public function deleted(User $user): void
{
Log::info("User {$user->id} deleted");
}
}Регистрация Observer
// app/Providers/EventServiceProvider.php
namespace App\Providers;
use App\Models\User;
use App\Observers\UserObserver;
class EventServiceProvider extends ServiceProvider
{
public function boot(): void
{
User::observe(UserObserver::class);
}
}Полезные методы в Observer
public function updating(User $user): void
{
// Проверка изменения атрибута
if ($user->isDirty('email')) {
// Email изменился
}
// Получить старое значение
$oldEmail = $user->getOriginal('email');
// Получить новое значение
$newEmail = $user->email;
}
public function updated(User $user): void
{
// Был ли атрибут изменён?
if ($user->wasChanged('email')) {
// Email точно изменился
}
// Какие атрибуты изменились?
$changed = $user->getChanges(); // ['email' => 'new@example.com']
}5. Event Subscribers — группировка слушателей
Когда использовать
Если у вас много событий и слушателей связанных с одной сущностью, удобнее группировать их в Subscriber.
Создание Subscriber
// app/Listeners/UserEventSubscriber.php
namespace App\Listeners;
use App\Events\UserRegistered;
use App\Events\UserLoggedIn;
use App\Events\UserUpdatedProfile;
use Illuminate\Events\Dispatcher;
use Illuminate\Support\Facades\Log;
class UserEventSubscriber
{
public function handleUserRegistration(UserRegistered $event): void
{
Log::info("User registered: {$event->user->email}");
// Логика обработки
}
public function handleUserLogin(UserLoggedIn $event): void
{
$event->user->update([
'last_login_at' => now()
]);
}
public function handleProfileUpdate(UserUpdatedProfile $event): void
{
// Очистка кеша
cache()->forget("user.{$event->user->id}.profile");
}
// Регистрация слушателей
public function subscribe(Dispatcher $events): array
{
return [
UserRegistered::class => 'handleUserRegistration',
UserLoggedIn::class => 'handleUserLogin',
UserUpdatedProfile::class => 'handleProfileUpdate',
];
}
}Регистрация Subscriber
// app/Providers/EventServiceProvider.php
protected $subscribe = [
UserEventSubscriber::class,
];6. Практические примеры
Пример 1: Система уведомлений
// Event
namespace App\Events;
class OrderCreated
{
public function __construct(
public Order $order
) {}
}
// Listeners
class SendOrderConfirmationEmail
{
public function handle(OrderCreated $event): void
{
Mail::to($event->order->user)
->send(new OrderConfirmation($event->order));
}
}
class NotifyAdminAboutOrder
{
public function handle(OrderCreated $event): void
{
$admins = User::where('role', 'admin')->get();
Notification::send($admins, new NewOrderNotification($event->order));
}
}
class UpdateInventory
{
public function handle(OrderCreated $event): void
{
foreach ($event->order->items as $item) {
$item->product->decrement('stock', $item->quantity);
}
}
}
// Регистрация
protected $listen = [
OrderCreated::class => [
SendOrderConfirmationEmail::class,
NotifyAdminAboutOrder::class,
UpdateInventory::class,
],
];
// Использование
public function store(Request $request)
{
$order = Order::create([
'user_id' => auth()->id(),
'total' => $request->total,
]);
event(new OrderCreated($order));
return redirect()->route('orders.show', $order);
}Пример 2: Аудит действий пользователя
// Observer
class PostObserver
{
public function created(Post $post): void
{
ActivityLog::create([
'user_id' => auth()->id(),
'action' => 'created',
'model_type' => Post::class,
'model_id' => $post->id,
'description' => "Created post: {$post->title}",
]);
}
public function updated(Post $post): void
{
$changes = $post->getChanges();
ActivityLog::create([
'user_id' => auth()->id(),
'action' => 'updated',
'model_type' => Post::class,
'model_id' => $post->id,
'description' => "Updated post: " . implode(', ', array_keys($changes)),
'metadata' => json_encode($changes),
]);
}
public function deleted(Post $post): void
{
ActivityLog::create([
'user_id' => auth()->id(),
'action' => 'deleted',
'model_type' => Post::class,
'model_id' => $post->id,
'description' => "Deleted post: {$post->title}",
]);
}
}Пример 3: Кеширование
class ProductObserver
{
public function created(Product $product): void
{
// Очистка кеша категории
cache()->forget("category.{$product->category_id}.products");
cache()->forget('products.latest');
}
public function updated(Product $product): void
{
// Очистка кеша продукта
cache()->forget("product.{$product->id}");
// Если категория изменилась
if ($product->wasChanged('category_id')) {
cache()->forget("category.{$product->getOriginal('category_id')}.products");
cache()->forget("category.{$product->category_id}.products");
}
}
public function deleted(Product $product): void
{
cache()->forget("product.{$product->id}");
cache()->forget("category.{$product->category_id}.products");
}
}7. Broadcast Events — real-time уведомления
Событие для broadcast
namespace App\Events;
use Illuminate\Broadcasting\Channel;
use Illuminate\Broadcasting\InteractsWithSockets;
use Illuminate\Contracts\Broadcasting\ShouldBroadcast;
use Illuminate\Queue\SerializesModels;
class MessageSent implements ShouldBroadcast
{
use InteractsWithSockets, SerializesModels;
public function __construct(
public Message $message
) {}
// На какой канал отправлять
public function broadcastOn(): Channel
{
return new Channel('chat.' . $this->message->chat_id);
}
// Что отправлять
public function broadcastWith(): array
{
return [
'id' => $this->message->id,
'text' => $this->message->text,
'user' => $this->message->user->only(['id', 'name']),
'created_at' => $this->message->created_at->toISOString(),
];
}
// Название события на frontend
public function broadcastAs(): string
{
return 'message.sent';
}
}Frontend получит это событие через WebSocket!
8. Лучшие практики
✅ DO (Делай так)
// ✅ Называй события в прошедшем времени
class UserRegistered {}
class OrderCreated {}
class PaymentProcessed {}
// ✅ Событие содержит только данные
class OrderCreated
{
public function __construct(
public Order $order
) {}
}
// ✅ Слушатели делают одно действие
class SendOrderConfirmationEmail
{
public function handle(OrderCreated $event): void
{
Mail::to($event->order->user)
->send(new OrderConfirmation($event->order));
}
}
// ✅ Используй очереди для медленных операций
class SendWelcomeEmail implements ShouldQueue
{
// ...
}
// ✅ Обрабатывай ошибки
class SendWelcomeEmail implements ShouldQueue
{
public function failed(UserRegistered $event, \Throwable $exception): void
{
Log::error('Failed to send welcome email', [
'user_id' => $event->user->id,
'error' => $exception->getMessage()
]);
}
}❌ DON'T (Не делай так)
// ❌ Не называй события как действия
class SendEmail {} // Плохо
class EmailSent {} // Хорошо
// ❌ Не помещай логику в события
class UserRegistered
{
public function sendEmail()
{
// Это должно быть в Listener!
}
}
// ❌ Не делай слушатель, который делает всё
class HandleEverythingAboutUser
{
public function handle(UserRegistered $event): void
{
$this->sendEmail($event->user);
$this->createProfile($event->user);
$this->giveBonus($event->user);
// Слишком много ответственности!
}
}
// ❌ Не забывай про очереди для медленных операций
class SendWelcomeEmail // Нет ShouldQueue
{
public function handle(UserRegistered $event): void
{
// Отправка email — медленная операция!
// Это замедлит регистрацию пользователя
Mail::to($event->user)->send(new WelcomeEmail($event->user));
}
}9. Debugging событий
Логирование всех событий
// app/Providers/EventServiceProvider.php
public function boot(): void
{
Event::listen('*', function ($eventName, array $data) {
if (!str_starts_with($eventName, 'Illuminate')) {
Log::debug("Event fired: {$eventName}", $data);
}
});
}Временное отключение событий
// Отключить все события
Event::fake();
// Отключить конкретные события
Event::fake([
UserRegistered::class,
OrderCreated::class,
]);
// Создать пользователя (события не сработают)
$user = User::create([...]);
// Проверить, что событие было бы вызвано
Event::assertDispatched(UserRegistered::class);Отключение для модели
// Временно без событий
User::withoutEvents(function () {
User::create([...]); // Observers не сработают
});
// Или
$user = User::create([...]);
$user->saveQuietly(); // Без события 'updated'10. Упражнения
Задание 1: Базовая система уведомлений
Создай систему, где при создании поста:
- Отправляется email автору с подтверждением
- Уведомляются все подписчики автора
- Пост добавляется в кеш "latest posts"
// Создай:
// - Event: PostPublished
// - Listener: SendPublishedNotification
// - Listener: NotifyFollowers
// - Listener: UpdateLatestPostsCacheЗадание 2: Аудит изменений
Создай Observer для модели Product, который логирует:
- Все изменения цены
- Создание нового продукта
- Удаление продукта
Лог должен содержать: кто изменил, когда, что изменилось.
Задание 3: Система лояльности
При создании заказа:
- Начисли пользователю баллы (10% от суммы)
- Если заказ > 1000₽, отправь "спасибо" email
- Если это первый заказ, дай бонус 100₽
- Обнови статистику (количество заказов пользователя)
Реализуй через Events и Listeners.
Задание 4: Event Subscriber
Создай UserEventSubscriber, который обрабатывает:
UserRegistered→ создаёт welcome запись в activity logUserLoggedIn→ обновляет last_login_atUserDeleted→ удаляет все связанные данные
11. Тестирование событий
Тест события
use Illuminate\Support\Facades\Event;
public function test_user_registration_dispatches_event()
{
Event::fake();
$user = User::factory()->create();
Event::assertDispatched(UserRegistered::class, function ($event) use ($user) {
return $event->user->id === $user->id;
});
}Тест слушателя
use Illuminate\Support\Facades\Mail;
public function test_welcome_email_sent_on_registration()
{
Mail::fake();
$user = User::factory()->create();
$listener = new SendWelcomeEmail();
$listener->handle(new UserRegistered($user));
Mail::assertSent(WelcomeEmail::class, function ($mail) use ($user) {
return $mail->hasTo($user->email);
});
}12. Шпаргалка
# Создание
php artisan make:event UserRegistered
php artisan make:listener SendWelcomeEmail --event=UserRegistered
php artisan make:observer UserObserver --model=User
# Запуск события
event(new UserRegistered($user));
UserRegistered::dispatch($user);
Event::dispatch(new UserRegistered($user));
# Очереди
class SendEmail implements ShouldQueue { }
# Отключение событий
Event::fake();
User::withoutEvents(fn() => User::create([...]));
$user->saveQuietly();
# Debugging
Event::listen('*', fn($event, $data) => Log::debug($event));🎯 Чеклист главы
- [ ] Понимаю разницу между Events и Listeners
- [ ] Умею создавать и регистрировать события
- [ ] Знаю когда использовать Observers
- [ ] Понимаю как работают Subscribers
- [ ] Умею делать асинхронную обработку
- [ ] Знаю best practices событийной архитектуры
- [ ] Умею тестировать события
Следующая глава: Глава 10.5: Task Scheduling — cron через Laravel, автоматизация задач
Событийная архитектура — мощный инструмент для создания гибких и расширяемых приложений. Она позволяет отделить бизнес-логику от побочных эффектов и легко добавлять новую функциональность без изменения существующего кода! 🚀