<? phpukraine СПІВБЕСІДИ
⌕Пошук по платформі
YII · JUNIOR ЧАСТО ПИТАЮТЬ

Як працюють правила валідації й сценарії моделей у Yii2?

`rules()` описує, що і як перевіряти, `scenarios()` - які атрибути активні в поточному сценарії й котрі з них safe; `load()` присвоює лише safe-атрибути, тому поле без жодного правила (або з `!` у сценарії) у модель просто не потрапить.

Чому після `$model->load(Yii::$app->request->post())` поле `bio` лишилось порожнім, хоча в POST воно точно є?
Навіщо `scenarios()`, якщо в правилі вже можна написати `'on' => 'signup'`?
Користувач підсунув у формі профілю `User[role]=admin`. Що станеться і як це закрити?
Ви дописали `scenarios()` у моделі, і тепер `save()` падає з `Unknown scenario: default`. Що не так?
Yii2 валідація rules scenarios mass assignment inline validator

Механіка простіша, ніж виглядає з документації. 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()` відповідають за різні речі: перший - за перевірки, другий - за список активних і safe-атрибутів; за замовчуванням `scenarios()` виводиться саме з `rules()`.
Що `load()` - це обгортка над `setAttributes($values, true)`, тому масове присвоєння обмежене `safeAttributes()`, а не наявністю публічної властивості.
Що атрибут без жодного правила не є safe, і щоб приймати його з форми, потрібен валідатор `safe`.
Що префікс `!` у `scenarios()` дає атрибут, який валідується, але не присвоюється з запиту - рівно те, що треба для `role`, `status`, `author_id`.
Сигнатуру inline-валідатора `($attribute, $params, $validator)` і те, що він за замовчуванням пропускається на порожньому значенні (`skipOnEmpty`) і після помилки іншого правила (`skipOnError`).
Перевизначити `scenarios()` без `parent::scenarios()` - зникає сценарій `default`, і `validate()` кидає `InvalidArgumentException: Unknown scenario: default`.
Думати, що `rules()` захищає від mass assignment. Правило `in` на `role` перевірить значення, але присвоїти його з форми все одно дозволить - обмежує лише `scenarios()`.
Дописати атрибут у `scenarios()` і чекати, що він почне валідуватись: без правила, активного в цьому сценарії, перевірок не буде взагалі.
Обходити safe-список руками: `$model->role = Yii::$app->request->post('role')`. Пряме присвоєння взагалі не проходить через `setAttributes()`.
Очікувати, що `required` відсіче `0` або `'0'`: `isEmpty()` вважає порожніми лише `null`, `''` і `[]`, тому нуль пройде.
Писати `load($data)` для плоского JSON без `formName`: без другого аргументу Yii шукає дані під ключем `User`, не знаходить, повертає `false` - і модель лишається порожньою.
ПОРАДА

Сформулюйте розподіл обовʼязків одним реченням: «`rules()` - про коректність значень, `scenarios()` - про те, які поля взагалі можна прийняти з запиту». Далі покажіть `'profile' => ['name', 'bio', '!role']` і скажіть, що це однорядкова відповідь на питання «а що, як юзер надішле role=admin».

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

`load()` викликає `setAttributes($values, true)`, а той звіряється з `safeAttributes()` - списком активних атрибутів сценарію без `!`-позначених. Наявність правила робить атрибут safe, відсутність правил або `!` - ні; жодних автоматичних `required` сценарій не додає.