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

Задача простая: форма обратной связи с полями «имя», «email» и «сообщение». Пользователь жмёт «Отправить», сервер проверяет данные, при ошибке подсвечивает поля, при успехе отправляет письмо. В Yii 2 есть три способа сделать это, и два из них ведут к боли: ручная проверка $_POST в контроллере и наспех собранный HTML без CSRF-токена. Ниже — как это устроено правильно, где проходит граница ответственности между моделью, контроллером и представлением, и чем платишь за каждый подход.
Материал написан для Yii 2.0.x (актуальная ветка на момент написания). В Yii 3 многое переехало в отдельные пакеты и работает иначе — здесь речь только про вторую версию.
Три подхода к форме и их цена
Прежде чем писать код, определитесь, где будет жить логика проверки. От этого зависит всё остальное.
| Подход | Что выигрываете | Чем платите |
|---|---|---|
Ручная проверка $_POST в контроллере | Ничего не нужно изучать, всё «на виду» | Валидация размазана по экшену, дублируется на каждой форме, CSRF и вывод ошибок пишете руками |
Модель-форма (yii\base\Model) + голый HTML | Правила в одном месте, переиспользуются | Разметку полей и ошибок пишете вручную, легко забыть токен |
Модель-форма + ActiveForm | Правила, вывод, ошибки и CSRF из коробки, клиентская валидация бесплатно | Нужно понять пару соглашений виджета; меньше контроля над разметкой |
Для типовой формы обратной связи выбор очевиден: модель плюс ActiveForm. Дальше разбираем именно его, а голый HTML показываю как контраст, чтобы было видно, что берёт на себя виджет.
Модель формы: где живёт валидация
Форма обратной связи не связана с таблицей в базе, поэтому наследуемся от yii\base\Model, а не от ActiveRecord. Правила проверки описываются в методе rules(), человеческие названия полей — в attributeLabels().
<?php
// models/ContactForm.php
namespace app\models;
use Yii;
use yii\base\Model;
class ContactForm extends Model
{
public $name;
public $email;
public $message;
public $verifyCode; // капча
// Правила проверки. Каждая строка: [атрибуты, валидатор, опции]
public function rules()
{
return [
// name, email, message обязательны
[['name', 'email', 'message'], 'required'],
// email должен быть корректным адресом
['email', 'email'],
// ограничиваем длину имени
['name', 'string', 'max' => 100],
// капча сверяется с встроенным экшеном
['verifyCode', 'captcha'],
];
}
// Подписи полей для формы и сообщений об ошибках
public function attributeLabels()
{
return [
'name' => 'Ваше имя',
'email' => 'Email для ответа',
'message' => 'Сообщение',
'verifyCode' => 'Проверочный код',
];
}
// Бизнес-действие: отправка письма после успешной валидации
public function sendEmail($toEmail)
{
return Yii::$app->mailer->compose()
->setTo($toEmail)
->setFrom([$this->email => $this->name])
->setSubject('Обращение с сайта')
->setTextBody($this->message)
->send();
}
}
Ключевая мысль: проверка данных — обязанность модели, а не контроллера. Когда вы вызываете $model->validate(), Yii прогоняет все правила из rules() и складывает ошибки в саму модель. Контроллеру остаётся спросить результат.
Если валидация написана в контроллере, вторая форма с теми же полями заставит скопировать проверку. Модель проверяет один раз и переиспользуется в веб-форме, в API и в консольной команде.
Что такое валидатор
Валидатор — это правило проверки одного или нескольких атрибутов. required, email, string, captcha — встроенные валидаторы Yii, каждому соответствует класс в пространстве yii\validators. Полный список — в разделе документации Core Validators; там же указано, какие опции принимает каждый.
Контроллер: собрать данные и решить, что дальше
Экшен делает три вещи: создаёт модель, пытается наполнить её из запроса, при успешной валидации выполняет действие. Ветвление на load() && validate() — стандартная связка Yii.
<?php
// controllers/SiteController.php (фрагмент)
use app\models\ContactForm;
use Yii;
public function actionContact()
{
$model = new ContactForm();
// load() возьмёт данные из POST по имени класса модели
// validate() прогонит rules(); оба true — данные корректны
if ($model->load(Yii::$app->request->post()) && $model->validate()) {
if ($model->sendEmail('support@example.com')) {
Yii::$app->session->setFlash('success', 'Сообщение отправлено.');
} else {
Yii::$app->session->setFlash('error', 'Не удалось отправить письмо.');
}
// редирект, чтобы обновление страницы не отправило форму заново
return $this->refresh();
}
// первый заход или ошибки валидации — показываем форму
return $this->render('contact', ['model' => $model]);
}
Обратите внимание: контроллер не проверяет отдельные поля и не собирает список ошибок. Он спрашивает validate() и работает с ответом. Если проверка не прошла, модель уже хранит тексты ошибок — представление их достанет.
Представление: ActiveForm против ручного HTML
Виджет ActiveForm сам подставляет CSRF-токен, генерирует разметку поля, выводит ошибку под ним и умеет клиентскую проверку без вашего JavaScript.
<?php
// views/site/contact.php
use yii\bootstrap5\ActiveForm; // или yii\widgets\ActiveForm без бутстрапа
use yii\helpers\Html;
use yii\captcha\Captcha;
/* @var $model app\models\ContactForm */
?>
<?php if (Yii::$app->session->hasFlash('success')): ?>
<div class="alert alert-success">
<?= Yii::$app->session->getFlash('success') ?>
</div>
<?php endif; ?>
<?php $form = ActiveForm::begin(['id' => 'contact-form']); ?>
<?= $form->field($model, 'name') ?>
<?= $form->field($model, 'email') ?>
<?= $form->field($model, 'message')->textarea(['rows' => 6]) ?>
<?= $form->field($model, 'verifyCode')->widget(Captcha::class) ?>
<div class="form-group">
<?= Html::submitButton('Отправить', ['class' => 'btn btn-primary']) ?>
</div>
<?php ActiveForm::end(); ?>
Здесь $form->field($model, 'name') знает имя поля, подпись из attributeLabels(), правила из rules() и, если валидация уже провалилась, текст ошибки. Одна строка закрывает всё это.
Для сравнения — как выглядело бы поле на голом HTML. CSRF-токен и вывод ошибки пришлось бы дописывать вручную:
<!-- То же поле без ActiveForm — руками -->
<label>Ваше имя</label>
<input type="text" name="ContactForm[name]"
value="<?= Html::encode($model->name) ?>">
<?php if ($model->hasErrors('name')): ?>
<div class="error"><?= Html::encode($model->getFirstError('name')) ?></div>
<?php endif; ?>
Разница видна: ActiveForm экономит десятки строк и не даёт забыть про экранирование и токен.
Капча и встроенный экшен
Правило ['verifyCode', 'captcha'] в модели работает только если в контроллере объявлен экшен, который рисует картинку и хранит код в сессии. Добавьте его в actions():
public function actions()
{
return [
'captcha' => [
'class' => 'yii\captcha\CaptchaAction',
],
];
}
Если экшена нет, валидатор captcha не найдёт эталонный код и форма не пройдёт проверку никогда. Для внутренних форм без спама капчу можно убрать и из rules(), и из представления.
Когда что выбирать
- Форма связана с таблицей (регистрация, редактирование профиля) — наследуйте модель от
ActiveRecord, правила пишите там же, вывод — тот жеActiveForm. - Форма без таблицы (обратная связь, поиск, фильтр) —
yii\base\Model, как в примере выше. - Форма отдаёт данные в API, а не в браузер — модель та же, но вместо рендера верните
$model->getErrors()в JSON;ActiveFormздесь не нужен. - Одно поле и никакой логики (например, строка поиска в шапке) — модель избыточна, обработайте
Yii::$app->request->get()напрямую.
Что ломается чаще всего
- Данные не загружаются.
load()ищет их по имени класса модели:ContactForm[name]. Если поля названы простоname,load()вернёт false. При ручном HTML легко ошибиться в имени. - Ошибка «Unable to verify your data submission». Отсутствует CSRF-токен.
ActiveFormставит его сам, при голом HTML добавьте скрытое поле черезYii::$app->request->csrfToken. - Атрибута нет в safe-полях.
load()присваивает только те атрибуты, что упомянуты вrules(). Если поле нигде не участвует в проверке, добавьте его в правилоsafe, иначе значение молча потеряется. - Капча не проходит. Нет экшена
captchaвactions()либо картинка и проверка живут в разных контроллерах — код в сессии не совпадёт. - Форма отправляется при F5. Нет редиректа после успешной обработки. Верните
$this->refresh()илиredirect(), а не рендер той же страницы.
Что делать дальше
Форма работает — теперь усильте её. Включите клиентскую валидацию (в ActiveForm она по умолчанию активна, проверьте, что подключён нужный ассет). Для сложных условий смотрите валидаторы compare, when и сценарии (scenarios()) — они позволяют менять набор правил для одной модели. Отдельная большая тема — собственные валидаторы через метод validateXxx в модели или отдельный класс: сюда рассматривать не стали, но начинать её изучение стоит с раздела документации о создании валидаторов.
Логотип и фирменный стиль: Три концепции разного характера, палитра с обоснованием, проверка на читаемость в малом размере и в один цвет.
Спросить за 60 ₽Источники: Yii 2 Guide — Collecting Tabular Input и Working with Forms, Yii 2 Guide — Core Validators
Частые вопросы
У меня Yii 3, этот код подойдёт?
load() возвращает false, форма не заполняется — в чём дело?
Значение поля теряется после отправки, хотя оно заполнено. Почему?
Можно ли вообще без ActiveForm, на голом HTML?
Как отдать ошибки валидации в JSON, а не рисовать страницу?
Капча всегда выдаёт ошибку, хотя код ввожу верно. Что проверить?
Материал носит информационный характер и подготовлен редакцией «Агентуры». Он не является офертой, рекламой или индивидуальной консультацией. Упомянутые продукты, компании и торговые знаки принадлежат их правообладателям. Перед принятием решений, влекущих юридические или финансовые последствия, обратитесь к профильному специалисту.