<? phpukraine СПІВБЕСІДИ
Пошук по платформі
YII · MIDDLE

Що таке behaviors і events у Yii2 і коли їх варто використовувати?

Event — точка розширення в `yii\base\Component`: клас викликає `trigger()`, а зовнішній код підписується через `on()`. Behavior — обʼєкт, який прикріплюється до компонента в рантаймі, підписує свої методи на події через `events()` і додає компоненту власні властивості й методи через `__get`/`__call`. Разом вони дають повторне використання поведінки без успадкування.

Чим behavior відрізняється від трейта і від спільного базового класу моделі?
Додали `TimestampBehavior`, а `created_at` лишається порожнім. Де шукати причину?
Треба логувати зміну статусу в десяти моделях. Як зробити це без копіювання коду?
`AccessControl` і `VerbFilter` у `behaviors()` контролера — це та сама механіка, що й `TimestampBehavior`?
Yii2 Behavior Event Component TimestampBehavior ActiveRecord

Обидва механізми живуть у yii\base\Component, і з цього краще починати відповідь. Клас, успадкований від yii\base\BaseObject, не має ні on(), ні attachBehavior(): BaseObject дає лише геттери/сеттери й конфігурування через конструктор. Компонент додає зверху дві речі. Подія - іменована точка, де клас сам себе перериває викликом trigger($name, $event) і пускає в цей момент зовнішню логіку. Поведінка влаштована інакше: це обʼєкт-мікшин, який прикріплюється до компонента в рантаймі, його метод events() повертає мапу «подія → обробник», а attach() розвішує підписки на власника.

Друга частина поведінки менш очевидна, і саме на ній ловлять. Прикріплена поведінка робить свої public-властивості та методи доступними через власника: Component::__get() і __call() перебирають $this->_behaviors і делегують виклик тому, хто його розуміє. Тому $post->slug працює через SluggableBehavior навіть без відповідного оголошення в класі моделі. Зворотний бік у тому, що магія спрацьовує лише коли в самому класі такого імені немає: якщо у вас уже є метод getStatus(), однойменний метод поведінки стане недосяжним мовчки, без жодного попередження. Додайте сюди ще й ліниву ініціалізацію: масив із behaviors() розбирається не в конструкторі, а в ensureBehaviors(), який викликається з __get, __call, trigger, on та кількох сусідніх методів.

На практиці Yii2 користується цією парою всюди, де ви й не думали про події. TimestampBehavior підписаний на EVENT_BEFORE_INSERT і EVENT_BEFORE_UPDATE і просто підставляє time() у два атрибути. BlameableBehavior бере Yii::$app->user->id. AttributeTypecastBehavior (з 2.0.10) приводить атрибути до типів після find() і перед записом, OptimisticLockBehavior (з 2.0.16) підтягує версію рядка. Фільтри контролера побудовані так само: yii\filters\AccessControl і VerbFilter успадковують ActionFilter, який успадковує Behavior і підписується на Controller::EVENT_BEFORE_ACTION, а скасування дії робиться через $event->isValid = false. Коли в behaviors() контролера ви пишете 'access' => [...], ви чіпляєте до контролера обʼєкт з обробником події, хоча виглядає це як налаштування фільтрів.

Ходом виконання керують два важелі, які регулярно плутають. $event->handled = true зупиняє виклик решти обробників цієї події, але сама операція триває далі. ModelEvent::$isValid = false (це подія beforeValidate, beforeSave, beforeDelete) скасовує операцію: save() поверне false, помилок валідації при цьому не буде, і в логах ви побачите тишу. Обробники викликаються в порядку прикріплення; on($name, $handler, $data, $append = false) ставить обробник першим, що іноді потрібно, коли ваша перевірка має відпрацювати до чужої. Для наскрізних сценаріїв є class-level підписки: Event::on(BaseActiveRecord::class, ...) спрацює для всіх нащадків, бо trigger() проходить ланцюжком класів власника.

Межа застосування проходить по двох ознаках: скільки класів потребують логіки і чи є в неї зовнішні ефекти. Якщо це одна модель і одна перевірка, пишіть beforeSave() прямо в моделі, так чесніше й читабельніше за окремий клас. Коли та сама логіка потрібна пʼятьом моделям або її треба вмикати конфігом чи знімати в тестах, беріть поведінку. А от події як механізм оркестрації бізнес-процесу («зберегли замовлення → списали склад → надіслали лист») дають приховану залежність: обробники синхронні, виняток в одному валить save(), а вже надісланий лист відкат транзакції не поверне. Для таких ефектів беруть чергу або outbox, лишаючи подіям те, що стосується самого рядка. І пильнуйте за обхідними шляхами: updateAll(), deleteAll(), updateAttributes() і insert() через createCommand() не піднімають подій узагалі, тому будь-яка гарантія, побудована на beforeSave, там просто не існує.

use yii\base\Behavior;
use yii\base\ModelEvent;
use yii\behaviors\TimestampBehavior;
use yii\db\ActiveRecord;
use yii\db\AfterSaveEvent;

/**
 * Пише в історію кожну зміну статусу.
 * @property-read Order $owner
 */
class StatusLogBehavior extends Behavior
{
    public string $attribute = 'status';

    /** @return array<string, string> мапа «подія → метод цієї поведінки» */
    public function events(): array
    {
        return [
            ActiveRecord::EVENT_BEFORE_UPDATE => 'guard',
            ActiveRecord::EVENT_AFTER_UPDATE => 'log',
        ];
    }

    public function guard(ModelEvent $event): void
    {
        // isValid = false скасовує save(), який поверне false без винятку
        if ($this->owner->{$this->attribute} === null) {
            $event->isValid = false;
        }
    }

    public function log(AfterSaveEvent $event): void
    {
        // changedAttributes містить СТАРІ значення змінених атрибутів
        if (!array_key_exists($this->attribute, $event->changedAttributes)) {
            return;
        }
        Yii::$app->db->createCommand()->insert('{{%status_log}}', [
            'order_id' => $this->owner->id,
            'from' => $event->changedAttributes[$this->attribute],
            'to' => $this->owner->{$this->attribute},
        ])->execute();
    }
}

class Order extends ActiveRecord
{
    public function behaviors(): array
    {
        return [
            // рядковий ключ потрібен, щоб зробити detachBehavior('timestamp')
            'timestamp' => TimestampBehavior::class,
            'statusLog' => ['class' => StatusLogBehavior::class, 'attribute' => 'status'],
        ];
    }
}
Що і події, і поведінки живуть у `yii\base\Component`, а не в `BaseObject`: клас, успадкований від `BaseObject`, їх не має взагалі.
Що поведінка — це не просто набір хуків: вона ще й «домішує» до власника свої public-властивості й методи, і саме тому `$model->slug` може працювати без колонки в класі.
Що фільтри контролера (`AccessControl`, `VerbFilter`, `Cors`) — це поведінки, підписані на `Controller::EVENT_BEFORE_ACTION` через `ActionFilter`.
Різницю між `$event->handled = true` (зупинити решту обробників) і `ModelEvent::$isValid = false` (скасувати саму операцію).
Що `updateAll()`, `deleteAll()` і `updateAttributes()` не піднімають подій, тому все, що ви повісили на `beforeSave`, там мовчки не спрацює.
Перевизначити `beforeSave()` і забути `return parent::beforeSave($insert);` — подію ніхто не тригерить, `TimestampBehavior` не працює, а `save()` ще й повертає `false`, бо метод повернув `null`.
Розраховувати, що поведінка перевизначить наявний метод моделі: `__get`/`__call` спрацьовують лише коли властивості чи методу в класі немає.
Оголошувати `behaviors()` без рядкових ключів, а потім не мати змоги зробити `detachBehavior('timestamp')` у сидері чи в тесті.
Слати листи або HTTP-запити з `afterSave`, вважаючи події асинхронними: обробник виконується синхронно, і якщо зовнішня транзакція відкотиться, лист уже пішов.
Викликати `Event::on()` у методі контролера або в `init()` моделі — при кожному виклику додається ще один обробник, і на третьому збереженні в лог падає три однакові записи.
ПОРАДА

Скажіть формулу: «подія — це точка розширення класу, поведінка — це набір обробників плюс властивості, які прикріплюються до обʼєкта в рантаймі». Далі дайте два орієнтири: якщо логіка потрібна одній моделі — `beforeSave()`, якщо тій самій логіці треба жити в пʼятьох моделях або вмикатися за конфігом — `Behavior`.

оновлено 5 вересня 2026 · ліцензія CC-BY-SA-4.0 Знайшли неточність? Напишіть →
ПЕРЕВІРТЕ СЕБЕ

Події й поведінки реалізовані саме в `yii\base\Component` (`BaseObject` їх не має), обробники виконуються синхронно, а поведінка — це окремий обʼєкт із власним станом, який прикріплюється до конкретного екземпляра й проксіюється магічними методами.