28Валидация
Правила rules(), сценарии и on/except, пустые значения и skipOnEmpty/skipOnError, условная валидация when/whenClient, фильтрация данных, ad hoc-валидация через DynamicModel, свои валидаторы (inline и класс), клиентская, отложенная и AJAX-валидация.
Данные извне не проверены, пока не прошли validate(). Правила описываются декларативно в rules(), ошибки собираются в модели, а те же правила автоматически превращаются в клиентскую JavaScript-проверку. Это единый механизм для форм, REST-запросов и консольных команд.
rules()#
public function rules()
{
return [
// [атрибуты, валидатор, опции...]
[['username', 'email', 'password'], 'required'],
['email', 'email'],
['username', 'string', 'min' => 2, 'max' => 24],
['username', 'unique', 'targetClass' => User::class],
['age', 'integer', 'min' => 18, 'message' => 'Только для взрослых'],
['status', 'in', 'range' => [self::STATUS_ACTIVE, self::STATUS_DELETED]],
['status', 'default', 'value' => self::STATUS_ACTIVE],
['website', 'url', 'defaultScheme' => 'https'],
['password_repeat', 'compare', 'compareAttribute' => 'password'],
[['first_name', 'last_name'], 'trim'],
['title', 'filter', 'filter' => 'strip_tags'],
['title', 'validateTitle'], // inline-валидатор — метод модели
['country', \app\components\validators\CountryValidator::class], // свой класс
['tags', 'safe'], // без проверки, но разрешить load()
['token', 'required', 'on' => 'register'], // только в сценарии
['email', 'unique', 'except' => 'update'], // во всех, кроме
];
}Валидатор указывается коротким именем (встроенные валидаторы), именем класса, объектом или именем метода модели. Правила применяются по порядку, атрибут проверяется всеми правилами, где он упомянут.
Запуск и ошибки#
$model = new ContactForm();
$model->load(Yii::$app->request->post());
if ($model->validate()) { // все атрибуты активного сценария
// ok
} else {
$model->errors; // ['email' => ['…', '…'], 'name' => ['…']]
$model->getFirstErrors(); // ['email' => '…']
$model->getFirstError('email');
$model->getErrorSummary(true); // плоский список
$model->hasErrors('email');
}
$model->validate(['email', 'name']); // только эти атрибуты
$model->addError('email', 'Такой уже есть'); // ошибка вручную
$model->clearErrors();ActiveRecord::save() вызывает validate() сам. В сообщениях доступны {attribute} (подпись из attributeLabels()) и {value}, у конкретных валидаторов — свои плейсхолдеры ({min}, {max}…).
Активные правила и атрибуты#
$model->scenario = 'register';
$model->activeAttributes(); // атрибуты, участвующие в сценарии
$model->getActiveValidators(); // валидаторы сценария
$model->getActiveValidators('email');
$model->validators; // ArrayObject всех валидаторов — можно добавить объект на летуПравило активно, если сценарий модели попадает в on (или on не задан) и не попадает в except. Атрибуты активных правил становятся безопасными для load() — кроме тех, что помечены восклицательным знаком в scenarios().
Пустые значения#
По умолчанию валидатор пропускает пустой атрибут (null, '', []) — проверяет только required. Меняется опцией:
['email', 'email', 'skipOnEmpty' => false],
['tags', 'each', 'rule' => ['string'], 'isEmpty' => function ($value) { return $value === [] || $value === null; }],По умолчанию Validator::isEmpty() считает пустыми null, '' и []. Ошибка при первом правиле атрибута приводит к пропуску остальных — skipOnError => false отключает это.
Условная валидация#
['state', 'required', 'when' => function ($model) {
return $model->country == 'USA';
}, 'whenClient' => "function (attribute, value) {
return $('#country').val() == 'USA';
}"],when — серверное условие; whenClient — то же на JS, чтобы клиентская валидация не спорила с серверной.
Фильтрация — не проверка, а преобразование#
[['username', 'email'], 'trim'],
['username', 'filter', 'filter' => 'strtolower'],
['title', 'filter', 'filter' => function ($value) { return mb_substr($value, 0, 100); }],
['status', 'default', 'value' => 1], // если пусто
['is_active', 'boolean'], // не преобразует! только проверяетtrim, filter, default меняют значение атрибута. Ставьте их перед проверяющими правилами.
Ad hoc-валидация#
Проверить значения без класса модели — DynamicModel:
public function actionSearch($name, $email)
{
$model = DynamicModel::validateData(compact('name', 'email'), [
[['name', 'email'], 'string', 'max' => 128],
['email', 'email'],
]);
if ($model->hasErrors()) {
// …
}
}
// или одно значение
$validator = new EmailValidator();
if ($validator->validate($email, $error)) { } else { echo $error; }DynamicModel можно создать и вручную: new DynamicModel(compact('name', 'email')) → addRule('email', 'email') → validate().
Свои валидаторы#
Inline — метод модели#
public function rules()
{
return [
['country', 'validateCountry'],
['token', function ($attribute, $params, $validator) {
if ($this->$attribute !== 'a') {
$this->addError($attribute, 'Неверный токен');
}
}],
];
}
public function validateCountry($attribute, $params, $validator)
{
if (!in_array($this->$attribute, ['USA', 'Indonesia'])) {
$validator->addError($this, $attribute, '{attribute} должна быть "USA" или "Indonesia".');
}
}Inline-валидаторы, как и все, пропускают пустые значения, если не задан skipOnEmpty => false. С 2.0.11 третьим аргументом приходит сам InlineValidator, поэтому удобнее $validator->addError($this, $attribute, $message) — работает с плейсхолдерами. Клиентская часть по умолчанию не генерируется, но её можно задать: ['country', 'validateCountry', 'clientValidate' => 'clientValidateCountry'], где метод возвращает JS-код.
Отдельный класс#
namespace app\components;
use yii\validators\Validator;
class CountryValidator extends Validator
{
public function validateAttribute($model, $attribute)
{
if (!in_array($model->$attribute, ['USA', 'Indonesia'])) {
$this->addError($model, $attribute, '{attribute} должна быть "USA" или "Indonesia".');
}
}
// чтобы валидатор работал и вне модели ($validator->validate($value, $error))
protected function validateValue($value)
{
return in_array($value, ['USA', 'Indonesia']) ? null : ['{attribute} должна быть "USA" или "Indonesia".', []];
}
}Достаточно переопределить validateValue() — validateAttribute() по умолчанию вызывает его. Если у валидатора есть опции, объявите их публичными свойствами и задайте умолчания в init().
Валидатор с несколькими атрибутами#
class MigrationRule extends Validator
{
public function validateAttribute($model, $attribute)
{
if ($model->$attribute == $model->otherAttribute) { … }
}
}
// применить один раз, а не для каждого атрибута:
[['from', 'to'], MigrationRule::class], // validateAttribute вызовется дважды
['from', MigrationRule::class], // один раз — ошибки добавить обоим вручнуюКлиентская валидация#
Встроенные валидаторы (кроме unique, exist, file частично и inline) генерируют JS через clientValidateAttribute(). Свой:
public function clientValidateAttribute($model, $attribute, $view)
{
$statuses = json_encode(Status::find()->select('id')->column());
$message = json_encode($this->message);
return <<<JS
if ($.inArray(value, $statuses) === -1) {
messages.push($message);
}
JS;
}В JS доступны attribute, value, messages, deferred, $form. Отключить: enableClientValidation => false у формы или 'enableClientValidation' => false в правиле.
Отложенная (deferred) валидация#
Для асинхронных проверок на клиенте (например, размер изображения):
var def = $.Deferred();
var img = new Image();
img.onload = function () {
if (this.width > 150) messages.push('Слишком широкое');
def.resolve();
};
img.src = URL.createObjectURL(file);
deferred.push(def);Форма ждёт все deferred, прежде чем показать результат.
AJAX-валидация#
Когда клиентской логики недостаточно (unique, сложные проверки):
// форма
$form = ActiveForm::begin(['enableAjaxValidation' => true]);
$form->field($model, 'username', ['enableAjaxValidation' => true]); // или для одного поля
// действие
if (Yii::$app->request->isAjax && $model->load(Yii::$app->request->post())) {
Yii::$app->response->format = Response::FORMAT_JSON;
return ActiveForm::validate($model);
}ActiveForm::validate($model, ['attr1', 'attr2']) вернёт ошибки в формате, который ждёт JS формы: второй аргумент — массив имён атрибутов. Несколько моделей перечисляют аргументами — ActiveForm::validate($user, $profile), а для табличного ввода есть ActiveForm::validateMultiple($models).
Порядок выполнения на практике#
load()— массовое присваивание безопасных атрибутов сценария.validate()→beforeValidate();false— прерывание.- Для каждого правила по порядку: пропуск пустых (
skipOnEmpty), пропуск с ошибками (skipOnError), условиеwhen, затем проверка или фильтрация. afterValidate(); результат —!hasErrors().
По умолчанию skipOnEmpty = true: из проверяющих валидаторов пустые значения смотрят только required и captcha. Задайте 'skipOnEmpty' => false, если нужно.
DynamicModel::validateData($data, $rules) или new DynamicModel() с addRule().
Опцией when (и whenClient для JS): 'when' => function ($model) { return $model->country == 'USA'; }.
Реализовать validateValue($value), возвращающий null либо [сообщение, параметры].
JS-код для него сгенерировать неоткуда. Задайте clientValidate в правиле (метод вернёт JS-код), вынесите проверку в класс-валидатор или включите AJAX-валидацию.