Обидва механізми живуть у 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'],
];
}
}
Скажіть формулу: «подія — це точка розширення класу, поведінка — це набір обробників плюс властивості, які прикріплюються до обʼєкта в рантаймі». Далі дайте два орієнтири: якщо логіка потрібна одній моделі — `beforeSave()`, якщо тій самій логіці треба жити в пʼятьох моделях або вмикатися за конфігом — `Behavior`.