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

В Yii 2 у вас есть модель Article, которой нужно автоматически проставлять created_at и updated_at, генерировать slug из заголовка и логировать каждое сохранение. Влить всё это в саму модель — значит раздуть класс и повторить тот же код в Comment, Page и десятке других сущностей. Поведения (behaviors) решают ровно эту задачу: прикрепляют методы, свойства и обработчики событий к компоненту снаружи, без наследования. Разберём приёмы, которые экономят время, и места, где behaviors ведут себя не так, как ожидаешь.
Статья про Yii 2 (ветка 2.0.x, актуальна для PHP 7.4+ и 8.x). Yii 1 и Yii 3 здесь не рассматриваем — там своя механика.
Что такое behavior и почему это не трейт
Behavior — это класс-наследник yii\base\Behavior, который через метод attach() цепляется к любому объекту-наследнику yii\base\Component. После прикрепления публичные методы и свойства поведения становятся доступны на самом компоненте, будто они там объявлены.
Трейт вкомпилирован в класс на этапе загрузки: подключить или отключить его в рантайме нельзя, конфликты имён разруливаются вручную, а слушать события трейт не умеет. Behavior подключается динамически, читается из конфига и умеет подписываться на события компонента. За это платите одним уровнем косвенности: вызов «чужого» метода идёт через __call, а обращение к свойству — через __get/__set.
Behavior добавляет объекту способности, о которых сам класс объекта ничего не знает. Это его сила и его же ловушка: метод есть, но в коде класса вы его не найдёте.
Термин
Событие (event) в Yii 2 — именованная точка в жизненном цикле объекта, на которую можно повесить обработчики. У ActiveRecord это, например, EVENT_BEFORE_INSERT, EVENT_AFTER_UPDATE. Behavior подписывается на такие события через метод events().
Подключение: три способа и когда какой
Behavior объявляют в методе behaviors() компонента. Возвращаемый массив может быть записан по-разному.
use yii\behaviors\TimestampBehavior;
use yii\behaviors\SluggableBehavior;
use yii\db\ActiveRecord;
class Article extends ActiveRecord
{
public function behaviors()
{
return [
// короткая форма: только имя класса, настройки по умолчанию
TimestampBehavior::class,
// с ключом и настройками — ключ нужен для доступа к поведению
'slug' => [
'class' => SluggableBehavior::class,
'attribute' => 'title',
'slugAttribute' => 'slug',
'immutable' => true, // не пересчитывать при обновлении
],
];
}
}Именованный ключ ('slug') даёт доступ к экземпляру поведения: $article->getBehavior('slug'). Без ключа вы поведение потом не достанете по имени — только переберёте все через getBehaviors(). Ключ обязателен, если вы планируете обращаться к конкретному behavior или переопределять его в дочернем классе.
Третий способ — прикрепить в рантайме, когда набор поведений зависит от условий:
$article->attachBehavior('audit', [
'class' => AuditBehavior::class,
'userId' => Yii::$app->user->id,
]);Динамическое подключение удобно для необязательной логики — например, аудит только для авторизованного пользователя. Но помните: если объект уже прошёл событие, на которое подписан behavior, задним числом обработчик не сработает.
Свой behavior: события и виртуальные свойства
Ключевой метод — events(). Он связывает событие компонента с методом поведения.
use yii\base\Behavior;
use yii\db\ActiveRecord;
class BlameableUpdateBehavior extends Behavior
{
public $createdByAttribute = 'created_by';
public $updatedByAttribute = 'updated_by';
public function events()
{
return [
ActiveRecord::EVENT_BEFORE_INSERT => 'onBeforeInsert',
ActiveRecord::EVENT_BEFORE_UPDATE => 'onBeforeUpdate',
];
}
public function onBeforeInsert($event)
{
// $this->owner — тот объект, к которому прикреплено поведение
$userId = \Yii::$app->has('user') ? \Yii::$app->user->id : null;
$this->owner->{$this->createdByAttribute} = $userId;
$this->owner->{$this->updatedByAttribute} = $userId;
}
public function onBeforeUpdate($event)
{
$userId = \Yii::$app->has('user') ? \Yii::$app->user->id : null;
$this->owner->{$this->updatedByAttribute} = $userId;
}
}Свойство $this->owner появляется после attach() и указывает на компонент. До прикрепления оно равно null — обращаться к нему в конструкторе поведения нельзя.
Публичное свойство и публичный метод поведения автоматически «проецируются» на владельца. Если объявить в behavior public $fullName и геттер getDisplayName(), то на модели заработают $article->fullName и $article->displayName — Yii разрешит их через __get. Это способ добавить виртуальное свойство, не трогая схему таблицы.
Порядок обработчиков и приоритеты
Одну и ту же точку жизненного цикла могут слушать несколько поведений плюс сам класс через init() или переопределённый beforeSave(). Порядок такой:
- обработчики событий вызываются в порядке подключения (behavior, подключённый раньше в массиве
behaviors(), отработает раньше); - методы самого класса вроде
beforeSave()вызываютparent::beforeSave(), внутри которого триггерится событие — то есть порядок относительно behavior зависит от того, до или послеparent::вы разместили свою логику; - если
beforeSave()вернётfalseдо вызоваparent::, событие вообще не поднимется и обработчики поведений не сработают.
Отсюда типичная ошибка: slug не генерируется, потому что валидация в beforeValidate() отклонила модель раньше, чем SluggableBehavior успел проставить значение. Смотрите на порядок событий, а не только на порядок строк в конфиге.
Встроенные поведения: что уже есть в ядре
| Behavior | Namespace | Задача |
|---|---|---|
| TimestampBehavior | yii\behaviors | Автозаполнение created_at / updated_at по событиям вставки и обновления |
| BlameableBehavior | yii\behaviors | Проставляет id текущего пользователя в created_by / updated_by |
| SluggableBehavior | yii\behaviors | Генерирует slug из атрибута, умеет обеспечивать уникальность |
| AttributeBehavior | yii\behaviors | Базовый класс: назначить значения атрибутам на заданных событиях |
| AttributeTypecastBehavior | yii\behaviors | Приведение типов атрибутов (строка из БД → int/bool) |
| OptimisticLockBehavior | yii\behaviors | Оптимистичная блокировка через version-поле |
Точный набор параметров каждого класса и их поведение между минорными версиями сверяйте в официальной документации Yii 2 — состав опций у SluggableBehavior и AttributeTypecastBehavior дополнялся в разных релизах ветки.
Что ломается на практике
- Конфликт имён. Если два поведения объявляют публичный метод с одинаковым именем, сработает первое по порядку подключения. Второе будет молча проигнорировано — ошибки не будет, а поведение окажется не тем. Давайте методам уникальные префиксы.
- Отключение по ключу без ключа.
detachBehavior('slug')сработает только если поведение подключено с ключом'slug'. Для безымянных придётся зватьdetachBehaviors()и цеплять заново нужные. - Свойства не видны в отладчике. Виртуальные свойства через
__getне отображаются вvar_dump($model)напрямую и не попадают вgetAttributes()— это не колонки таблицы. Ловят это, когда сериализуют модель в JSON и «пропадает» вычисляемое поле. - owner равен null. Обращение к
$this->ownerв конструкторе поведения даёт fatal error: объект ещё не прикреплён. Инициализируйте зависимое от owner состояние в переопределённомattach($owner), вызвав сначалаparent::attach($owner). - Событие не поднялось. Если
beforeSave()вернулfalseраньшеparent::beforeSave(), весь блок обработчиков поведений на этом событии пропускается. Признак — timestamp не проставился, хотя TimestampBehavior подключён.
Что делать дальше
Если поведение начинает знать слишком много о конкретной модели (лезет в её связи, специфичные поля), это сигнал вынести логику в отдельный сервис, а behavior оставить тонкой прослойкой над событиями. Для повторяющегося аудита посмотрите на связку BlameableBehavior + TimestampBehavior — вместе они закрывают четыре системных поля без единой строки в модели. Следующий шаг для наведения порядка — вынести общий набор поведений в базовый ActiveRecord-класс проекта и переопределять его в наследниках через именованные ключи. И проверьте в отладочной панели Yii, сколько раз реально триггерятся ваши события на одном запросе: лишние срабатывания в цикле сохранения — частый источник незаметной просадки.
Контент-конвейер: Из одной идеи — текст, изображение, озвучка и ролик. Один заказ вместо четырёх.
Спросить за 60 ₽Источники: Yii 2 Guide — Behaviors, Yii 2 API — yii\base\Behavior
Частые вопросы
Чем behavior отличается от трейта в PHP, если оба переиспользуют код?
Как обратиться к прикреплённому поведению из модели?
У меня другая версия Yii — приёмы сработают?
Почему TimestampBehavior не проставил created_at, хотя подключён?
Виртуальное свойство из поведения не попадает в JSON модели — почему?
Можно ли подключать поведение по условию, только для части объектов?
Материал носит информационный характер и подготовлен редакцией «Агентуры». Он не является офертой, рекламой или индивидуальной консультацией. Упомянутые продукты, компании и торговые знаки принадлежат их правообладателям. Перед принятием решений, влекущих юридические или финансовые последствия, обратитесь к профильному специалисту.