Поведения и события в Yii: чек-лист перед продакшеном
Behavior и события в Yii 2 удобно навешивают логику на модели и контроллеры, но перед боем стоит проверить порядок вызова, утечки замыканий и производительность. Разбираем, что ломается первым.

Behavior и события — то, что делает Yii 2 гибким: одним attachBehavior вы навешиваете на модель мягкое удаление, версионирование или логирование, не трогая её код. Проблема в том, что удобство скрывает цену. На ревью я регулярно вижу код, где обработчик события дёргает базу в цикле, а поведение молча ломает валидацию, потому что его подключили не в том порядке. Этот чек-лист — не введение в тему, а то, что нужно проверить перед выкладкой на прод. Актуально для Yii 2.0.x (проверял на 2.0.49).
Порядок навешивания решает всё
Поведение в Yii — это объект, который подписывается на события компонента через метод events(). Если два поведения слушают одно событие, они вызовутся в порядке подключения. Это первая вещь, которая ломается тихо.
Классический пример — TimestampBehavior и собственное поведение, которое читает updated_at в обработчике EVENT_BEFORE_UPDATE. Если ваше поведение подключено раньше TimestampBehavior, оно увидит старое значение времени. Yii не выдаст ошибку — просто в логах будет неверная метка.
Порядок обработчиков одного события — это не деталь реализации, а часть контракта. Если вы полагаетесь на него неявно, ближайший рефакторинг метода behaviors() сломает вам логику без единого исключения.
Проверьте в методе behaviors() модели: поведения, которые мутируют атрибуты, должны идти до тех, что эти атрибуты читают. Порядок задаётся порядком ключей в массиве.
// app\models\Post, Yii 2.0.x
public function behaviors()
{
return [
// Сначала проставляем метки времени
'timestamp' => [
'class' => \yii\behaviors\TimestampBehavior::class,
],
// Потом поведение, которое их читает
'audit' => [
'class' => app\behaviors\AuditBehavior::class,
],
];
}
Замыкания в обработчиках держат объекты в памяти
Yii позволяет подписаться на событие анонимной функцией через on(). Удобно, но у этого есть цена: замыкание захватывает контекст, и если внутри вы используете $this или тяжёлый объект, он не освободится, пока живёт компонент, к которому привязан обработчик.
В обычном веб-запросе это не заметно — процесс завершается. Но в консольных командах, воркерах очередей и долгоживущих процессах (например, под RoadRunner или в daemon-режиме) это превращается в утечку. Проверьте все вызовы on() вне контроллеров: если обработчик навешивается в цикле или на объект, который создаётся многократно, вы копите ссылки.
- Обработчик, подписанный через
Component::on(), живёт, пока жив компонент — глобальные события наYii::$appживут весь запрос. - Если подписались в цикле и не сняли обработчик через
off(), при следующей итерации событие вызовет и старый обработчик тоже — так рождаются двойные записи в лог. - Замыкание с
use ($heavyObject)держит объект до отписки. В воркере это память, которая не вернётся.
Как проверить
Найдите по проекту все вызовы ->on( и Event::on(. Для каждого ответьте: сколько раз выполнится эта строка за жизнь процесса и снимается ли обработчик. Если строка в цикле или в методе, вызываемом многократно, — нужен off() либо подписка один раз при старте.
Тяжёлая работа в обработчике блокирует основной сценарий
Событие EVENT_AFTER_INSERT — популярное место, чтобы отправить письмо, дёрнуть внешний API или пересчитать агрегат. Обработчик выполняется синхронно, в той же транзакции сохранения. Если внутри вы делаете HTTP-запрос к платёжному шлюзу на 800 мс, ваш save() будет висеть эти 800 мс, и клиент увидит тормозящую форму.
Хуже, если обработчик кидает исключение: в зависимости от того, где открыта транзакция, вы получите либо откат сохранения (данные не записались, хотя пользователь увидел успех до этого), либо частично применённые изменения.
Что делать с тяжёлыми обработчиками
- Вынесите долгую работу в очередь. Событие только ставит задачу — публикация в очередь занимает единицы миллисекунд, а не сотни.
- Не делайте сетевые вызовы внутри транзакции сохранения. Если письмо всё-таки шлётся синхронно, отправляйте его после коммита, а не в
EVENT_AFTER_INSERT, который может выполниться внутри транзакции. - Обработчик, который может упасть, оберните так, чтобы его падение не роняло основную операцию, если она не обязана зависеть от него. Логируйте исключение, но не глотайте молча.
Валидация и dirty-атрибуты
Поведения, меняющие атрибуты в EVENT_BEFORE_VALIDATE, влияют на то, какие поля попадут в правила валидации. А те, что пишут в EVENT_BEFORE_INSERT или EVENT_BEFORE_UPDATE, добавляют атрибуты в список изменённых уже после валидации.
Частая ошибка — поведение проставляет author_id в beforeSave, но правило required для этого поля срабатывает раньше, на валидации, и сохранение падает. Решение — либо снять правило и доверять поведению, либо ставить значение в EVENT_BEFORE_VALIDATE, а не в EVENT_BEFORE_INSERT.
| Событие | Когда срабатывает | Для чего использовать |
|---|---|---|
| EVENT_BEFORE_VALIDATE | До проверки правил | Проставить значения, которые участвуют в валидации |
| EVENT_BEFORE_INSERT | После валидации, до записи новой строки | Метки времени, значения не из формы |
| EVENT_BEFORE_UPDATE | После валидации, до обновления | Обновление служебных полей |
| EVENT_AFTER_INSERT | После успешной записи | Постановка задач в очередь, не тяжёлая работа |
Глобальные события через DI и Yii::$app
Подписка на события в файле конфигурации через on-секцию компонента выглядит аккуратно, но выполняется при каждом запросе, даже если событие в этом запросе не наступит. Само по себе это дёшево, пока обработчик — простое замыкание. Но если в конфиге вы создаёте объект-обработчик через Yii::createObject, он инстанцируется на каждый запрос независимо от того, сработает событие или нет.
Событие, которое стреляет раз в тысячу запросов, не должно стоить инициализации на каждом из тысячи. Ленивое создание обработчика важнее, чем красивая секция в конфиге.
Чек-лист перед выкладкой
- Порядок поведений в
behaviors()проверен: мутирующие идут раньше читающих. - Все
on()вне контроллеров пересмотрены на предмет повторной подписки и утечек замыканий. - В обработчиках нет синхронных сетевых вызовов внутри транзакции.
- Обработчики
afterInsert/afterUpdateлибо быстрые, либо ставят задачу в очередь. - Поля, которые проставляет поведение, не конфликтуют с
required-правилами. - Исключения в обработчиках либо обрабатываются, либо осознанно роняют основную операцию.
- Тяжёлые объекты-обработчики создаются лениво, а не на каждый запрос.
Что мерить в проде
Поставьте таймер на время выполнения save() у ключевых моделей — если оно скачет, ищите обработчик, который делает что-то синхронно. Включите профилирование в панели отладки Yii (yii2-debug) на стейджинге и посмотрите, сколько событий стреляет на типовой запрос: неожиданно большое число часто означает двойную подписку. Для воркеров очередей следите за ростом памяти процесса между задачами — плавный рост почти всегда указывает на обработчик, который копит ссылки.
Тендерный специалист 44-ФЗ / 223-ФЗ: Разбор PDF-документации закупки, поиск «заточек» под поставщика, чек-лист заявки.
Спросить за 20 ₽Источники: Yii 2 Guide — Behaviors, Yii 2 Guide — Events
Частые вопросы
У меня Yii 2.0.13, а не 2.0.49 — чек-лист подойдёт?
Обработчик события не срабатывает, хотя я его подписал — где искать?
Грозит ли двойная подписка на событие потерей данных?
Можно ли отменить сохранение из обработчика события?
Что лучше — поведение или переопределение метода вроде beforeSave?
Как безопасно снять обработчик, подписанный замыканием?
Материал носит информационный характер и подготовлен редакцией «Агентуры». Он не является офертой, рекламой или индивидуальной консультацией. Упомянутые продукты, компании и торговые знаки принадлежат их правообладателям. Перед принятием решений, влекущих юридические или финансовые последствия, обратитесь к профильному специалисту.