Зачем нужны события:
Представьте метод контроллера, который проводит оплату заказа. После оплаты надо отправить чек на почту, уведомить менеджера в Telegram, начислить бонусы и сбросить кэш статистики. Если писать все подряд, метод распухнет до сотни строк, а каждая новая хотелка будет лезть правками в оплату, самое опасное место проекта. События разворачивают зависимость: код оплаты сообщает "заказ оплачен" и идет дальше, а кто на это подписан, его не касается.
Код: Выделить всё
php artisan make:event OrderPaid
php artisan make:listener SendOrderReceipt --event=OrderPaid
Код: Выделить всё
// app/Events/OrderPaid.php
class OrderPaid
{
use Dispatchable, SerializesModels;
public function __construct(public Order $order) {}
}
Код: Выделить всё
// app/Listeners/SendOrderReceipt.php
class SendOrderReceipt
{
public function handle(OrderPaid $event): void
{
Mail::to($event->order->user)->send(new OrderReceipt($event->order));
}
}
Код: Выделить всё
OrderPaid::dispatch($order);
Слушатель в очереди:
По умолчанию слушатели выполняются синхронно, прямо в запросе. Отправка письма занимает секунды, держать ради нее пользователя глупо. Добавьте интерфейс ShouldQueue, и слушатель уедет в очередь из главы 10 без единой лишней строчки:
Код: Выделить всё
class SendOrderReceipt implements ShouldQueue
{
use InteractsWithQueue;
public int $tries = 3;
public function viaQueue(): string
{
return 'mail';
}
public function failed(OrderPaid $event, Throwable $e): void
{
Log::error('Чек не отправлен', ['order_id' => $event->order->id]);
}
}
Наблюдатели моделей:
Eloquent и сам бросает события на каждый чих: creating, created, updating, updated, deleting, deleted. Подписываться на них удобнее не россыпью слушателей, а наблюдателем, классом, где собраны реакции на жизненный цикл одной модели:
Код: Выделить всё
php artisan make:observer ProductObserver --model=Product
Код: Выделить всё
class ProductObserver
{
public function saved(Product $product): void
{
Cache::forget('catalog.menu');
}
public function deleted(Product $product): void
{
Cache::forget('catalog.menu');
}
}
Код: Выделить всё
use Illuminate\Database\Eloquent\Attributes\ObservedBy;
#[ObservedBy([ProductObserver::class])]
class Product extends Model
Кеширование:
Меню каталога строится тяжелым запросом и меняется пару раз в день. Считать его заново на каждый запрос расточительно. Фасад Cache:
Код: Выделить всё
use Illuminate\Support\Facades\Cache;
$menu = Cache::remember('catalog.menu', now()->addHours(6), function () {
return Category::query()
->whereNull('parent_id')
->with('children')
->get();
});
Где данные лежат физически, решает драйвер. С Laravel 11 по умолчанию стоит database, таблица cache уже есть в стартовых миграциях, для начала хватает. Драйвер file пишет в storage/framework/cache и бесполезен, когда серверов больше одного: у каждого свой диск и свой кэш. Для прода ставьте Redis: расширение phpredis из репозитория дистрибутива и CACHE_STORE=redis в .env. Redis держит данные в памяти, один на все серверы, и его же вы уже используете под очереди.
Инвалидация:
Главный вопрос кэширования не "как положить", а "когда сбросить". Стратегии три. Короткий TTL: данные отстают на пару минут, для счетчиков и статистики нормально. Явный forget() в наблюдателе модели: данные всегда свежие ценой связки кэша с моделью. Тегированный кэш через Cache::tags() сбрасывает группы ключей разом, но работает только на redis и memcached.
С версии 11.23 есть еще Cache::flexible(), он убирает толпу на протухшем ключе:
Код: Выделить всё
$menu = Cache::flexible('catalog.menu', [3600, 21600], fn () => Category::tree());
И не зовите Cache::flush() или php artisan cache:clear на проде по привычке: они сносят стор целиком, не глядя на префиксы, включая локи планировщика из главы 16.
Логирование:
Каналы описаны в config/logging.php. По умолчанию активен stack, который пишет в один файл storage/logs/laravel.log. На проде первым делом переключайтесь на daily, тот же лог, но с ротацией по дням и автоудалением старых файлов:
Код: Выделить всё
LOG_CHANNEL=daily
LOG_DAILY_DAYS=14
Код: Выделить всё
Log::info('Заказ оплачен', ['order_id' => $order->id, 'amount' => $order->total]);
Log::channel('slack')->critical('Платежный шлюз недоступен');
Код: Выделить всё
// в middleware
Context::add('request_id', (string) Str::uuid());
Свои исключения:
Генерятся командой php artisan make:exception. У класса исключения могут быть методы report() и render(), фреймворк найдет их сам: report() решает, как событие попадет в лог, render() какой ответ увидит пользователь.
Код: Выделить всё
class PaymentGatewayException extends Exception
{
public function report(): void
{
Log::channel('slack')->critical($this->getMessage());
}
public function render(Request $request): Response
{
return response()->view('errors.payment', [], 502);
}
}
Код: Выделить всё
->withExceptions(function (Exceptions $exceptions) {
$exceptions->dontReport(PaymentDeclinedException::class);
$exceptions->level(PDOException::class, LogLevel::CRITICAL);
})
Разбор ошибок на проде:
APP_DEBUG=false на проде, всегда. С true Laravel отдает любому посетителю трассировку, переменные окружения и куски кода. Пользователь видит страницу 500, а вы идете в storage/logs/laravel-2026-06-11.log. Запись начинается строкой с датой, уровнем и сообщением, дальше стектрейс. Читайте трейс сверху вниз до первого файла из app/, это почти всегда и есть виновник, остальное служебные вызовы фреймворка. Локально удобен laravel/pail (ставится с флагом --dev): php artisan pail --level=error показывает лог в реальном времени с фильтрами. На сервере те же задачи закрывают tail -f и grep.
Типичные грабли:
Массовые операции мимо наблюдателей. Product::where('old', true)->update(['archived' => true]) не создает моделей и не бросает событий Eloquent: наблюдатель молчит, кэш не сброшен. Касается и update, и delete через билдер. Либо обходите записи коллекцией, либо сбрасывайте кэш руками рядом с запросом. Обратная сторона той же медали: saveQuietly() сохраняет модель, не будя наблюдателей, выручает в сидерах и миграциях данных.
Слушатель в очереди и старый код. Queued-слушатель, это обычная задача очереди: после деплоя воркер выполняет старую версию класса, пока не сделаете php artisan queue:restart. Грабли из главы 10, но на слушателях про них стабильно забывают.
Секреты в логах. Не логируйте request()->all() на формах с паролями и картами. Один забытый Log::debug, и пароли пользователей лежат в laravel.log открытым текстом.
debug на проде. LOG_LEVEL=debug пишет все подряд, диск кончается в самый неудачный момент. Для прода info, а лучше warning, если info-шума много.
Итог: события развязывают код, наблюдатели вешают реакции на жизненный цикл моделей, кэш экономит базу, а нормально настроенные логи отвечают на вопрос "что случилось ночью" за минуты, а не часы. На этом курс действительно закончен. Дальше практика: возьмите пет-проект и прогоните его через все восемнадцать глав, от миграций до деплоя с очередями и алертами в Slack.