Механіка простіша, ніж виглядає з документації. rules() повертає плоский список правил виду [атрибути, валідатор, опції], де валідатором буває псевдонім вбудованого класу (required, string, integer, email, url, boolean, in, match, compare, date, unique, exist, file, image, each, default, filter, trim, safe), назва методу самої моделі або анонімна функція. Кожне правило Yii перетворює в обʼєкт yii\validators\Validator, але validate() застосовує тільки активні: спершу зіставляє опції on/except правила з поточним сценарієм, потім перевіряє, чи входить атрибут у activeAttributes(). Обидві умови мають виконатись, тому правило, яке начебто є в коді, може ніколи не спрацювати.
Далі йде scenarios(). Якщо його не перевизначати, Yii сам збирає сценарії з правил: усе без on іде в default, а on => 'signup' створює сценарій signup. Перевизначають його тоді, коли набір полів для екрана вужчий за набір колонок. І тут же ховається найдорожча помилка: scenarios() без parent::scenarios() затирає default, після чого будь-який new User() із дефолтним сценарієм валиться на validate() з Unknown scenario: default. Тому метод майже завжди починають з батьківського масиву й дописують до нього свої ключі.
load($data, $formName = null) робить дві речі: шукає $data[$formName] (за замовчуванням formName() повертає коротке ім'я класу, звідси User[email] у HTML-полях) і, якщо знайшов, викликає setAttributes($values, true). Другий аргумент true означає safe-only: присвоюється тільки те, що є в safeAttributes(). Атрибут без жодного правила туди не потрапляє, ось чому bio «не зберігається» без будь-якої помилки, поки ви не додасте [['bio'], 'safe']. Для API з плоским JSON передають порожній formName: $user->load($data, '').
Обернена потреба теж трапляється: заборонити присвоєння, але залишити валідацію. Для цього перед іменем атрибута в scenarios() (або в самому правилі) ставлять !: 'profile' => ['name', 'bio', '!role']. Тепер role перевіряється валідатором in, але спроба надіслати User[role]=admin з форми профілю нічого не змінить: onUnsafeAttribute() тихо відкине значення й у debug-режимі залишить запис у логу. Присвоювати таке поле має код: $user->role = User::ROLE_USER. І пам'ятайте, що пряме $user->role = $post['role'] обходить механізм повністю, бо setAttributes() там немає.
Власні перевірки починаються з inline-валідатора: правило [['password'], 'validatePasswordStrength'] вказує на метод моделі з сигнатурою ($attribute, $params, $validator), а помилку додають через addError(). Чи викличеться він узагалі, визначають дві успадковані опції: skipOnEmpty (true) пропускає порожні значення, skipOnError (true) пропускає атрибут, який уже провалив попереднє правило. Коли та сама логіка потрібна у трьох моделях, inline виносять в окремий клас-нащадок yii\validators\Validator з методом validateAttribute($model, $attribute) і підключають за іменем класу; заодно з'являється місце для клієнтської валідації через clientValidateAttribute().
namespace app\models;
use yii\db\ActiveRecord;
class User extends ActiveRecord
{
public const SCENARIO_SIGNUP = 'signup';
public const SCENARIO_PROFILE = 'profile';
public $password; // віртуальний атрибут: у таблиці лише password_hash
public function rules(): array
{
return [
[['email', 'name'], 'trim'], // FilterValidator, працює до інших правил
[['email', 'name'], 'required'],
[['email'], 'email'],
[['email'], 'unique'], // на update сам виключить поточний рядок
[['name'], 'string', 'min' => 2, 'max' => 64],
[['bio'], 'safe'], // без цього рядка load() проігнорує bio
[['role'], 'in', 'range' => ['user', 'editor', 'admin']],
[['password'], 'required', 'on' => self::SCENARIO_SIGNUP],
[['password'], 'string', 'min' => 10, 'on' => self::SCENARIO_SIGNUP],
[['password'], 'validatePasswordStrength', 'on' => self::SCENARIO_SIGNUP],
];
}
public function scenarios(): array
{
$scenarios = parent::scenarios(); // інакше зникне сценарій default
$scenarios[self::SCENARIO_SIGNUP] = ['email', 'name', 'password'];
$scenarios[self::SCENARIO_PROFILE] = ['name', 'bio', '!role'];
return $scenarios; // role перевіряється, але з форми не присвоюється
}
/** Inline-валідатор: не викликається, якщо password порожній або вже має помилку */
public function validatePasswordStrength(string $attribute, ?array $params, $validator): void
{
if (!preg_match('/\d/', (string) $this->$attribute)) {
$this->addError($attribute, 'Пароль має містити хоча б одну цифру.');
}
}
}
// Контролер: load() візьме лише safe-атрибути сценарію profile - name і bio
$user = User::findOne((int) $id);
$user->scenario = User::SCENARIO_PROFILE;
if ($user->load(Yii::$app->request->post()) && $user->save()) { /* ... */ }
Сформулюйте розподіл обовʼязків одним реченням: «`rules()` - про коректність значень, `scenarios()` - про те, які поля взагалі можна прийняти з запиту». Далі покажіть `'profile' => ['name', 'bio', '!role']` і скажіть, що це однорядкова відповідь на питання «а що, як юзер надішле role=admin».