Поведения и события в Yii: когда что выбирать и чем платить
В Yii 2 одну и ту же логику можно повесить через behavior, через on/trigger или через хардкод в модели. Разбираем, что выигрываешь и чем платишь в каждом случае.

Вам нужно проставлять created_at и updated_at при сохранении записи. Первое, что подсказывает интуиция — переопределить beforeSave() в модели и записать туда две строки. Работает. Но через полгода такой же таймстамп нужен в пяти других моделях, а к нему добавляются slug, автор записи и запись в лог активности. И вот beforeSave() раздувается до тридцати строк, а копипаста расползлась по проекту.
В Yii 2 (актуальная ветка на момент написания — 2.0.x) под эту задачу есть два штатных механизма: поведения (behaviors) и события (events). Плюс всегда есть вариант «просто написать в модели». Все три решают перекрывающийся круг задач, но платите вы за них по-разному. Разберём, где граница.
Три способа повесить логику на сохранение
Вариант 1 — код прямо в модели
Самый прямой путь. Переопределяем метод жизненного цикла и пишем логику внутри:
// app\models\Article, Yii 2.0.x
public function beforeSave($insert)
{
if (!parent::beforeSave($insert)) {
return false;
}
$now = time();
if ($insert) {
$this->created_at = $now;
}
$this->updated_at = $now;
return true;
}Что выигрываете: всё в одном месте, читается сверху вниз, ничего не нужно знать про инфраструктуру фреймворка. Чем платите: логика намертво привязана к этой модели. Хотите то же в Comment — копируете. Забыли вызвать parent::beforeSave() — тихо сломали цепочку событий, и другие обработчики (в том числе чужие поведения) не отработают.
Вариант 2 — поведение (behavior)
Поведение — это объект, который «подмешивает» свойства и методы в компонент и умеет сам подписываться на его события. Для таймстампов у Yii уже есть готовое yii\behaviors\TimestampBehavior:
// app\models\Article, Yii 2.0.x
use yii\behaviors\TimestampBehavior;
public function behaviors()
{
return [
[
'class' => TimestampBehavior::class,
// атрибуты по умолчанию created_at / updated_at,
// здесь можно переопределить имена
],
];
}Что выигрываете: одна строка на модель, ноль копипасты, поведение переиспользуется в любой из десяти моделей. Логика инкапсулирована и тестируется отдельно. Чем платите: чтобы понять, что происходит при сохранении, нужно открыть behaviors(), а потом — исходник поведения. Для новичка в проекте это «магия»: атрибут заполняется, а где — непонятно.
Вариант 3 — обработчик события через on()
Событие — это точка, в которой компонент кричит «я собираюсь сохраниться», а кто угодно снаружи может на этот крик отреагировать. Подписка идёт через on(), а внутри компонент запускает событие через trigger():
// где-то при инициализации, Yii 2.0.x
use yii\db\ActiveRecord;
use yii\db\BaseActiveRecord;
$article = new Article();
$article->on(BaseActiveRecord::EVENT_BEFORE_INSERT, function ($event) {
// $event->sender — сам объект Article
$event->sender->created_at = time();
});Что выигрываете: обработчик живёт снаружи модели, модель о нём не знает. Идеально, когда на сохранение нужно повесить постороннюю логику: отправить письмо, инвалидировать кеш, дёрнуть внешний сервис — то, что к самой модели отношения не имеет. Чем платите: если подписка делается вручную на конкретном экземпляре, легко забыть подписать новый объект. Для «на все экземпляры разом» нужна подписка на уровне класса — Event::on(Article::class, ...) — и это отдельная тема, которую здесь не разворачиваем.
Как эти три штуки связаны между собой
Ключевой момент, который путает новичков: поведения и события — не альтернативы, а слои. TimestampBehavior внутри себя подписывается ровно на те же EVENT_BEFORE_INSERT и EVENT_BEFORE_UPDATE. То есть поведение — это упакованный, переиспользуемый обработчик событий с готовой конфигурацией.
Поведение отвечает на вопрос «какую логику подмешать в этот класс». Событие отвечает на вопрос «в какой момент жизни объекта её выполнить». Хардкод в модели отвечает на оба разом — и именно поэтому не масштабируется.
Метод beforeSave() в базовом ActiveRecord сам вызывает $this->trigger(EVENT_BEFORE_INSERT) или EVENT_BEFORE_UPDATE. Поэтому, переопределив beforeSave() и забыв parent::, вы гасите событие — и все поведения, что на него подписаны, молчат.
Таблица: что где выбирать
| Критерий | Код в модели | Поведение | Событие через on() |
|---|---|---|---|
| Логика нужна в одной модели | Подходит | Избыточно | Избыточно |
| Одинаковая логика в 3+ моделях | Копипаста | Лучший выбор | Возможно, но громоздко |
| Логика посторонняя (письмо, кеш) | Захламляет модель | Годится, если переиспользуется | Лучший выбор |
| Нужны новые свойства/методы на объекте | Пишутся напрямую | Умеет подмешивать | Не умеет |
| Порог входа для читающего код | Низкий | Средний (нужно знать механизм) | Средний |
| Тестируется изолированно | Тяжело | Легко | Средне |
Когда что выбирать — короткое правило
- Логика уникальна для одной модели и относится к её сути (например, пересчёт итоговой суммы заказа из его позиций) — пишите в модели, в
beforeSave()или в отдельном методе. Не плодите поведение ради единственного места. - Одна и та же служебная логика повторяется в нескольких моделях (таймстампы, slug, автор, soft-delete) — выносите в поведение. Это его прямое назначение.
- Логика посторонняя для модели и завязана на момент (после сохранения заказа — уведомить склад) — вешайте обработчик события. Модель не должна знать про склад.
Практический критерий на один вопрос: «относится ли эта логика к тому, чем модель является?» Если да и она уникальна — в модель. Если да, но повторяется — в поведение. Если нет — в событие.
Что ломается на практике
Три ошибки, которые встречаются чаще остального.
- Забытый parent:: в переопределённом методе. Написали
beforeSave()безparent::beforeSave($insert)— иTimestampBehavior, который вы подключили строкой выше, перестал заполнять таймстампы. Ошибки нет, просто в базеcreated_atравен нулю. Всегда вызывайте родителя и проверяйте его результат. - Порядок обработчиков. На одно событие можно навесить несколько обработчиков. Они выполняются в порядке подписки. Если поведение A зависит от результата поведения B, а подписаны они наоборот — получите тонкий баг, который в тестах на одном объекте не воспроизводится.
- Подписка на экземпляр вместо класса.
$model->on(...)действует только на этот объект. Создали новый черезnew Article()илиArticle::findOne()— он про обработчик не знает. Если логика нужна для всех записей класса, это признак того, что вам нужно поведение или подписка на уровне класса, а неon()на одном объекте.
Что делать дальше
Если вы сейчас копипастите одну и ту же логику по beforeSave() нескольких моделей — вынесите её в поведение и удалите дубли, это самая быстрая победа. Посмотрите готовые поведения в пространстве yii\behaviors: кроме TimestampBehavior там есть BlameableBehavior (автор записи) и SluggableBehavior — возможно, ваш велосипед уже написан. Полный список событий ActiveRecord и порядок их вызова смотрите в разделе про события официального гайда — именно там видно, между какими шагами вклинивается ваш обработчик.
Создатель презентаций: Структура и текст каждого слайда: заголовки-утверждения, одна мысль на слайд, заметки докладчику.
Спросить за 150 ₽Источники: Yii 2.0 Guide — Behaviors, Yii 2.0 Guide — Events
Частые вопросы
У меня старая версия Yii, не 2.0 — это сработает?
Поведение подключил, а таймстамп всё равно не заполняется — почему?
Можно ли навесить несколько обработчиков на одно событие?
Событие через on() на экземпляре или на классе — в чём разница?
Что лучше для производительности — поведение или код в модели?
Материал носит информационный характер и подготовлен редакцией «Агентуры». Он не является офертой, рекламой или индивидуальной консультацией. Упомянутые продукты, компании и торговые знаки принадлежат их правообладателям. Перед принятием решений, влекущих юридические или финансовые последствия, обратитесь к профильному специалисту.