События в Yii 2: типичные ошибки и как их избежать
Обработчик, который не срабатывает, утечка памяти при отписке через анонимную функцию, событие, вызванное в конструкторе, — разбираем ошибки при работе с событиями в Yii 2 и как их чинить.

Вы повесили обработчик на EVENT_AFTER_SAVE, а он не срабатывает. Или срабатывает дважды. Или вы пытаетесь отписаться через off(), а обработчик остаётся живым. Всё это — типичные грабли при работе с событиями в Yii 2 (актуально для версии 2.0.x, включая 2.0.49). Ниже — конкретные ошибки с кодом, который их вызывает, и как переписать, чтобы работало.
Речь только о событиях компонентов (yii\base\Component) и их частном случае — событиях ActiveRecord. Механизм behaviors как источник событий затрагиваем по касательной; поведения — отдельная тема.
Как события работают в Yii 2 — коротко
Любой класс, унаследованный от yii\base\Component, умеет три вещи: подписывать обработчик через on(), отписывать через off() и запускать событие через trigger(). Обработчики хранятся внутри объекта в массиве, ключом которого служит имя события — обычная строка.
Именно строковая природа имени события и объектная привязка обработчиков дают большинство ошибок. Разберём их по одной.
Ошибка 1. Подписка на событие класса, а не экземпляра
Метод on() у объекта вешает обработчик только на этот экземпляр. Если вы создаёте модель заново на каждый запрос, обработчик, повешенный на предыдущий объект, ни при чём не поможет.
// Не сработает для новых экземпляров:
$user = new User();
$user->on(User::EVENT_AFTER_INSERT, function ($event) {
// выполнится только для этого $user
});
$another = new User(); // здесь обработчика уже нет
Чтобы обработчик срабатывал для всех экземпляров класса, подписку вешают на уровне класса статическим методом Event::on():
use yii\base\Event;
use app\models\User;
// Сработает для любого экземпляра User:
Event::on(
User::class,
User::EVENT_AFTER_INSERT,
function ($event) {
// $event->sender — конкретный объект User
Yii::info('Создан пользователь #' . $event->sender->id);
}
);
Такую подписку удобно регистрировать в методе bootstrap() приложения или в init() компонента, чтобы она выполнялась один раз при старте.
Правило простое: событие конкретного объекта —$object->on(). Событие всех объектов класса —Event::on(Class::class, ...). Их легко перепутать, и обработчик молча не вызывается.
Ошибка 2. Опечатка в имени события
Имя события — строка. Yii не проверяет, существует ли такое событие: on() спокойно принимает любую строку и запоминает её. Если вы написали 'afterSave' вместо константы EVENT_AFTER_SAVE (значение которой — 'afterSave' совпадает, но в других событиях расходится), обработчик повиснет на несуществующее имя и никогда не вызовется.
// Плохо: строка руками, легко ошибиться
$model->on('afterSaved', [$this, 'handle']); // опечатка, тихо не сработает
// Хорошо: константа, IDE подскажет и опечатку не даст
$model->on(ActiveRecord::EVENT_AFTER_UPDATE, [$this, 'handle']);
Всегда используйте константы EVENT_* вместо строковых литералов. Диагностировать такую ошибку тяжело: ни исключения, ни предупреждения — просто ничего не происходит.
Ошибка 3. Невозможность отписаться от анонимной функции
Метод off() отписывает обработчик по совпадению. Если вы подписались анонимной функцией, у вас нет ссылки на неё — и отписаться точечно уже нельзя.
// Подписались анонимкой — ссылки не сохранили
$component->on(Component::EVENT_INIT, function ($e) { /* ... */ });
// Так не отпишется — это другая анонимная функция, не та же:
$component->off(Component::EVENT_INIT, function ($e) { /* ... */ });
Если обработчик придётся снимать — сохраните его в переменную:
$handler = function ($e) { /* ... */ };
$component->on(Component::EVENT_INIT, $handler);
// Теперь отписка сработает — передаём ту же ссылку:
$component->off(Component::EVENT_INIT, $handler);
Вызов off() с двумя аргументами (объект и имя события без обработчика) снимает все обработчики этого события — иногда это то, что нужно, но чаще так сносят чужие подписки.
Ошибка 4. Обработчик мешает сохранению, потому что не проверяет флаг
Некоторые события «отменяемые»: у объекта ModelEvent есть свойство isValid. Если в обработчике EVENT_BEFORE_INSERT или EVENT_BEFORE_VALIDATE выставить $event->isValid = false, дальнейшая операция прервётся. Частая ошибка — не понимать, что ваш обработчик тихо ломает сохранение.
$model->on(ActiveRecord::EVENT_BEFORE_INSERT, function ($event) {
if (empty($event->sender->email)) {
// Это остановит insert() и вернёт false
$event->isValid = false;
}
});
Если save() внезапно возвращает false без ошибок валидации — проверьте, не ставит ли какой-то обработчик isValid = false.
Ошибка 5. Тяжёлая логика в обработчике синхронного события
Обработчики вызываются синхронно, прямо внутри trigger(). Если в EVENT_AFTER_INSERT вы отправляете письмо через внешний SMTP, каждый save() ждёт ответа почтового сервера. Пользователь видит подвисший запрос.
Долгие операции — отправка писем, обращения к внешним API, генерация PDF — выносите в очередь (например, через расширение yii2-queue), а в обработчике только ставьте задачу. Так trigger() отработает быстро.
Ошибка 6. Порядок обработчиков не гарантирован так, как вы думаете
По умолчанию on() добавляет обработчик в конец списка — они вызываются в порядке подписки. Но четвёртый аргумент on() со значением false ставит обработчик в начало. Если вы полагаетесь на порядок, а где-то в коде кто-то подписался «в начало», логика поедет.
// Четвёртый аргумент false — вставка в начало очереди
$component->on(Component::EVENT_INIT, [$this, 'first'], null, false);
Не выстраивайте бизнес-логику на неявном порядке обработчиков. Если порядок критичен — объедините логику в один обработчик.
Сводка ошибок
| Симптом | Причина | Что делать |
|---|---|---|
| Обработчик не вызывается для новых объектов | Подписка на экземпляр вместо класса | Использовать Event::on(Class::class, ...) |
| Обработчик молча не срабатывает | Опечатка в имени события | Использовать константы EVENT_* |
off() не снимает обработчик | Подписка анонимной функцией без сохранения ссылки | Сохранять обработчик в переменную |
save() возвращает false без ошибок | Обработчик ставит isValid = false | Проверить логику before-событий |
| Запрос подвисает при сохранении | Тяжёлая логика в синхронном обработчике | Вынести в очередь |
Термин на всякий случай
* trigger — метод trigger($name, $event = null), который запускает событие: перебирает всех подписанных обработчиков этого имени и вызывает их по очереди. Пока обработчик не завершится, следующий не начнётся — отсюда синхронность.
Что делать дальше
Соберите все подписки уровня класса в одном месте — например, в bootstrap() приложения, чтобы не искать их по проекту. Для отладки поставьте временный Yii::info() в начало обработчика и проверьте лог: если записи нет — событие не долетело, ищите ошибку из списка выше. Если планируете много обработчиков на модели — посмотрите в сторону behaviors: они инкапсулируют подписку и снимаются вместе с поведением. Точные сигнатуры методов on(), off() и trigger() для вашей версии сверяйте в документации класса yii\base\Component.
Проектная работа: От проблемы до защиты: продукт, этапы, вопросы комиссии
Спросить за 15 ₽Источники: Yii 2 Guide — Events, Yii 2 API — yii\base\Component
Частые вопросы
У меня Yii 2.0.14, а не свежая версия — приёмы подойдут?
Обработчик не вызывается, хотя код правильный. Что проверить первым?
Событие сработало дважды — почему?
Можно ли отменить сохранение из обработчика afterSave?
Долгая отправка письма в обработчике тормозит сайт. Как быть в проде?
Как снять сразу все обработчики события?
Материал носит информационный характер и подготовлен редакцией «Агентуры». Он не является офертой, рекламой или индивидуальной консультацией. Упомянутые продукты, компании и торговые знаки принадлежат их правообладателям. Перед принятием решений, влекущих юридические или финансовые последствия, обратитесь к профильному специалисту.