Формы и валидация в Yii 2: разбираем на живом примере
Модель формы, правила валидации и вывод ошибок в Yii 2 на примере регистрации. Где ActiveForm экономит время, а где скрытая логика правил бьёт по проекту.

Форма регистрации принимает email и пароль, вы вешаете правило ['email', 'email'] — и радуетесь, что валидация работает. А потом приходит баг: пользователь ввёл email в верхнем регистре, в базе уже есть такой же в нижнем, unique-проверка его пропустила, вставка упала на уникальном индексе с 500-й ошибкой. Разберём, как в Yii 2 устроены формы и валидация на примере регистрации, где правила срабатывают в ожидаемом порядке, а где — нет.
Статья про Yii 2 (ветка 2.0.x, актуально для 2.0.49+). Yii 3 к моменту написания в бета-стадии, там модель форм устроена иначе — здесь речь только о второй ветке. Про клиентскую валидацию через ActiveForm говорим кратко: основной разбор — серверная сторона, потому что именно ей вы доверяете данные.
Две модели формы: ActiveRecord и Model
Первое решение, которое влияет на весь остальной код, — от чего наследовать форму. Вариантов ровно два, и путать их — частая ошибка.
- yii\db\ActiveRecord — если форма один-в-один ложится на таблицу. Правила описывают колонки,
save()пишет в базу. - yii\base\Model — если полей больше или меньше, чем колонок: подтверждение пароля, капча, «согласен с условиями». Такую модель называют form model.
Регистрация — классический случай для отдельной form model. В таблице user хранится password_hash, а форма принимает password и password_repeat, которых в базе нет. Наследуйте от ActiveRecord — и придётся объявлять несуществующие атрибуты и следить, чтобы они не попали в save(). Отдельная модель чище.
<?php
namespace app\models;
use Yii;
use yii\base\Model;
class RegistrationForm extends Model
{
public $email;
public $password;
public $passwordRepeat;
public $agree;
public function rules()
{
return [
// обязательные поля
[['email', 'password', 'passwordRepeat'], 'required'],
// формат email и нормализация регистра
['email', 'email'],
['email', 'filter', 'filter' => 'strtolower'],
// уникальность по чужой таблице через targetClass
['email', 'unique',
'targetClass' => User::class,
'targetAttribute' => 'email',
'message' => 'Этот email уже занят',
],
['password', 'string', 'min' => 8],
// повтор должен совпасть с паролем
['passwordRepeat', 'compare', 'compareAttribute' => 'password'],
// чекбокс согласия обязателен и должен быть true
['agree', 'required', 'requiredValue' => 1,
'message' => 'Нужно принять условия'],
];
}
public function attributeLabels()
{
return [
'passwordRepeat' => 'Повторите пароль',
];
}
}Порядок правил: не тот, что вы написали
Главная ловушка. Кажется, что валидаторы отрабатывают сверху вниз по массиву rules(). Это не так. Yii группирует правила по атрибутам и внутри одного атрибута идёт по порядку, но filter — не совсем валидатор: он меняет значение атрибута. Поэтому важно, что filter с strtolower стоит после email, но до unique. Если unique сработает раньше нормализации регистра, вы проверите на уникальность непреобразованное значение — и получите тот самый баг из первого абзаца.
Проверить порядок легко: временно поменяйте местами filter и unique, зарегистрируйте Test@Mail.ru, потом test@mail.ru — во втором случае увидите, прошла проверка или нет.
filter — это не проверка, а изменение данных. Держите его перед любым правилом, которое зависит от финального значения атрибута, иначе будете валидировать одно, а сохранять другое.
Контроллер: load, validate, а потом всё остальное
Три вызова, которые встречаются в любом обработчике формы Yii. Порядок фиксированный.
$model->load(Yii::$app->request->post())— загружает данные из POST в атрибуты. Возвращаетtrue, только если в запросе есть данные под именем модели. Первый заход на страницу (GET) вернётfalse— так вы отличаете показ формы от её отправки.$model->validate()— прогоняет правила. Возвращаетtrue/false, ошибки складывает в$model->errors.- Только после успешной валидации — бизнес-логика: хэширование пароля, создание User, отправка письма.
<?php
namespace app\controllers;
use Yii;
use yii\web\Controller;
use app\models\RegistrationForm;
use app\models\User;
class AuthController extends Controller
{
public function actionRegister()
{
$form = new RegistrationForm();
// load вернёт false на первом GET-заходе
if ($form->load(Yii::$app->request->post()) && $form->validate()) {
$user = new User();
$user->email = $form->email;
// хэш пароля считаем здесь, не в форме
$user->password_hash = Yii::$app->security
->generatePasswordHash($form->password);
if ($user->save()) {
Yii::$app->session->setFlash('success', 'Регистрация прошла');
return $this->redirect(['auth/login']);
}
}
// сюда попадаем при GET и при ошибке валидации
return $this->render('register', ['model' => $form]);
}
}Почему load возвращает false, хотя данные пришли
Самая частая жалоба новичков: форма отправлена, а load() не грузит. Причина в formName(). По умолчанию Yii ждёт данные в POST под ключом с именем класса — RegistrationForm[email], а не просто email. Виджет ActiveForm генерирует такие имена сам. Если вы пишете HTML руками или шлёте JSON, ключи не совпадут, и load() молча вернёт false.
Два выхода: используйте ActiveForm, который расставит имена правильно, либо передайте имя формы вторым аргументом — $form->load($data, '') с пустой строкой снимает префикс и грузит данные с верхнего уровня. Второй способ удобен для API.
Представление: ActiveForm и вывод ошибок
Виджет ActiveForm связывает поля с атрибутами модели, подтягивает attributeLabels() и по умолчанию включает клиентскую валидацию — то есть дублирует часть правил в JavaScript. Это удобно, но помните: клиентская проверка — только для UX, доверять ей нельзя, сервер всё равно валидирует заново.
<?php
use yii\bootstrap5\ActiveForm;
use yii\helpers\Html;
$form = ActiveForm::begin(['id' => 'register-form']);
?>
<?= $form->field($model, 'email') ?>
<?= $form->field($model, 'password')->passwordInput() ?>
<?= $form->field($model, 'passwordRepeat')->passwordInput() ?>
<?= $form->field($model, 'agree')->checkbox() ?>
<?= Html::submitButton('Зарегистрироваться', ['class' => 'btn btn-primary']) ?>
<?php ActiveForm::end(); ?>Класс виджета зависит от подключённого пакета: yii\bootstrap5\ActiveForm, yii\bootstrap4\ActiveForm или базовый yii\widgets\ActiveForm без стилей. Ставьте из того namespace, который реально стоит в composer.json проекта, иначе получите ошибку класса.
Сценарии: одна модель — разные наборы правил
Если та же модель используется и при регистрации, и при смене пароля, где email трогать не надо, разведите правила через сценарии. У правила указывается 'on' => 'scenarioName', а перед валидацией задаётся $model->scenario = 'name'. Без сценария правило работает во всех сценариях — легко забыть и провалидировать лишнее. Это отдельная большая тема, здесь только отмечаем как выход из ситуации «одна форма, два контекста».
Кастомный валидатор: когда встроенных мало
Встроенных валидаторов Yii хватает на большинство случаев — required, email, string, compare, unique, match по регулярке. Когда нужна своя логика (например, запретить одноразовые email-домены), пишите метод-валидатор прямо в модели и вешайте его как inline-правило.
public function rules()
{
return [
// ...прочие правила
['email', 'validateDomain'],
];
}
// сигнатура inline-валидатора: имя атрибута и параметры
public function validateDomain($attribute, $params)
{
$banned = ['mailinator.com', 'tempmail.com'];
$domain = substr(strrchr($this->$attribute, '@'), 1);
if (in_array(strtolower($domain), $banned, true)) {
// ошибка привязывается к атрибуту, не бросается исключением
$this->addError($attribute, 'Одноразовые почты не принимаем');
}
}Ключевое: валидатор не бросает исключение и не возвращает false, а вызывает addError(). Если ошибок не добавлено — проверка считается пройденной.
Если не получилось
- load() возвращает false, форма не грузится. Проверьте имена полей в POST: должны быть вида
RegistrationForm[email]. Для голого HTML или JSON передайте вторым аргументом пустое имя формы. - Валидация проходит, а в базу падает дубликат. Правило
filterс нормализацией стоит послеunique. Переставьте filter выше зависимого правила. - Ошибки не выводятся под полями. Поля добавлены через
Html::input(), а не через$form->field(). Толькоfield()подключает блок ошибок. - compare молчит, хотя пароли разные. Проверьте
compareAttribute: там имя свойства модели (password), а не подпись поля. - Class not found на ActiveForm. Подключён namespace от пакета, которого нет в проекте. Сверьте установленный adapter в composer.json.
Что дальше
Форму собрали — теперь стоит закрыть три вещи. Первая: включите фильтрацию входных данных (trim на строковых полях как отдельное правило) — пробелы по краям email ломают unique. Вторая: если форма уходит в API, разберитесь с formName() и загрузкой без префикса, чтобы клиент слал плоский JSON. Третья: посмотрите механизм сценариев — как только модель начнёт обслуживать больше одного контекста, без них правила расползутся. Отправная точка для всего перечисленного — раздел Input Validation в официальном гайде Yii 2.
Защитник прав потребителей: Досудебные претензии на возврат товара со ссылками на ЗоЗПП.
Спросить за 15 ₽Источники: Yii 2 Guide — Validating Input, Yii 2 Guide — Creating Forms
Частые вопросы
Наследовать форму от ActiveRecord или от Model?
У меня Yii 3, здесь код заработает?
Почему load() возвращает false, хотя я отправил форму?
Клиентской валидации ActiveForm достаточно, чтобы не проверять на сервере?
Как сделать своё правило, которого нет среди встроенных?
Одна модель нужна для регистрации и для смены email с разными правилами — как быть?
Материал носит информационный характер и подготовлен редакцией «Агентуры». Он не является офертой, рекламой или индивидуальной консультацией. Упомянутые продукты, компании и торговые знаки принадлежат их правообладателям. Перед принятием решений, влекущих юридические или финансовые последствия, обратитесь к профильному специалисту.