Проверка входных данных

Как правило, вы никогда не должны доверять данным, полученным от конечных пользователей, и всегда должны проверять его, прежде чем использовать его.

Для модели, заполненной пользовательскими вводами, вы можете проверить входные данные, вызвав метод yii\base\Model::validate(). Метод вернет логическое значение, указывающее, успешно ли выполнена проверка. Если нет, вы можете получить сообщения об ошибках из свойства yii\base\Model::$errors. Например:

$model = new \app\models\ContactForm();

// populate model attributes with user inputs
$model->load(\Yii::$app->request->post());
// which is equivalent to the following:
// $model->attributes = \Yii::$app->request->post('ContactForm');

if ($model->validate()) {
    // all inputs are valid
} else {
    // validation failed: $errors is an array containing error messages
    $errors = $model->errors;
}

Объявление правил

Чтобы заставить validate () работать действительно, вы должны объявить правила проверки для атрибутов, которые вы планируете проверять. Это должно быть сделано путем переопределения метода yii\base\Model::rules(). В следующем примере показано, как объявляются правила проверки для модели ContactForm:

public function rules()
{
    return [
        // the name, email, subject and body attributes are required
        [['name', 'email', 'subject', 'body'], 'required'],

        // the email attribute should be a valid email address
        ['email', 'email'],
    ];
}

Метод rules() должен возвращать массив правил, каждый из которых представляет собой массив следующего формата:

[
    // required, specifies which attributes should be validated by this rule.
    // For a single attribute, you can use the attribute name directly
    // without having it in an array
    ['attribute1', 'attribute2', ...],

    // required, specifies the type of this rule.
    // It can be a class name, validator alias, or a validation method name
    'validator',

    // optional, specifies in which scenario(s) this rule should be applied
    // if not given, it means the rule applies to all scenarios
    // You may also configure the "except" option if you want to apply the rule
    // to all scenarios except the listed ones
    'on' => ['scenario1', 'scenario2', ...],

    // optional, specifies additional configurations for the validator object
    'property1' => 'value1', 'property2' => 'value2', ...
]

Для каждого правила вы должны указать, по крайней мере, какие атрибуты применяет правило и какой тип правила. Вы можете указать тип правила в одной из следующих форм:

  • Псевдоним основного валидатора, например, required, in, date и т. д.
  • Имя метода проверки в классе модели или анонимную функцию. 
  • Полное имя класса валидатора. 

Правило может использоваться для проверки одного или нескольких атрибутов, а атрибут может быть проверен одним или несколькими правилами. Правило может применяться в определенных сценариях только путем указания опции включения. Если вы не укажете опцию on, это означает, что правило будет применено ко всем сценариям.

Когда вызывается метод validate(), для выполнения проверки выполняются следующие шаги:

  1. Определите, какие атрибуты должны проверяться путем получения списка атрибутов из yii\base\Model::scenarios() с использованием текущего сценария. Эти атрибуты называются активными атрибутами.
  2. Определите, какие правила проверки следует использовать, получая список правил из yii\base\Model::rules() с использованием текущего сценария. Эти правила называются активными правилами.
  3. Используйте каждое активное правило для проверки каждого активного атрибута, связанного с этим правилом. Правила проверки проверяются в порядке их перечисления.

Согласно вышеописанным шагам проверки атрибут будет проверен тогда и только тогда, когда он является активным атрибутом, объявленным в scenarios(), и связан с одним или несколькими активными правилами, объявленными в rules().

Настройка сообщений об ошибках

Большинство валидаторов имеют сообщения об ошибках по умолчанию, которые будут добавлены в проверяемую модель, если ее атрибуты не пройдут проверку. Например, требуемый валидатор добавит сообщение «Имя пользователя не может быть пустым». К модели, когда атрибут username выдает ошибку с использованием этого валидатора.

Вы можете настроить сообщение об ошибке правила, указав свойство message при объявлении правила, например:

public function rules()
{
    return [
        ['username', 'required', 'message' => 'Please choose a username.'],
    ];
}

Некоторые валидаторы могут поддерживать дополнительные сообщения об ошибках, чтобы точнее описывать различные причины сбоев валидации. Например, средство проверки числа поддерживает tooBig и tooSmall, чтобы описать отказ проверки, когда проверяемое значение слишком велико и слишком мало, соответственно. Вы можете настроить эти сообщения об ошибках, например, настроить другие свойства валидаторов в правиле проверки.

События проверки

Когда вызывается yii\base\Model::validate(), он вызовет два метода, которые вы можете переопределить, чтобы настроить процесс валидации:

  • yii\base\Model::beforeValidate(): реализация по умолчанию вызовет событие yii\base\Model::EVENT_BEFORE_VALIDATE. Вы можете либо переопределить этот метод, либо ответить на это событие, чтобы выполнить некоторую работу по препроцессорному процессу (например, нормализовать ввод данных) до того, как произойдет проверка. Метод должен возвращать логическое значение, указывающее, следует ли продолжать проверку или нет.
  • yii\base\Model::afterValidate(): реализация по умолчанию вызовет событие yii\base\Model::EVENT_AFTER_VALIDATE. Вы можете либо переопределить этот метод, либо ответить на это событие, чтобы выполнить некоторую постобработку после завершения проверки.

Условная проверка

Проверка атрибутов только при определенных условиях, например. Проверка одного атрибута зависит от значения другого атрибута, который можно использовать для свойства when, чтобы определить такие условия. Например:

  ['state', 'required', 'when' => function($model) {
        return $model->country == 'USA';
    }]

Свойство when принимает вызов PHP со следующей сигнатурой:

/**
 * @param Model $model the model being validated
 * @param string $attribute the attribute being validated
 * @return bool whether the rule should be applied
 */
function ($model, $attribute)

Если вам также нужно поддерживать условную проверку на стороне клиента, вы должны настроить свойство whenClient, которое принимает строку, представляющую функцию JavaScript, возвращаемое значение которой определяет, применять это правило или нет. Например:

  ['state', 'required', 'when' => function ($model) {
        return $model->country == 'USA';
    }, 'whenClient' => "function (attribute, value) {
        return $('#country').val() == 'USA';
    }"]

Фильтрация данных

Входы пользователей часто необходимо фильтровать или предварительно обрабатывать. Например, вы можете обрезать пробелы вокруг username. Для достижения этой цели вы можете использовать правила проверки.

В следующих примерах показано, как обрезать пробелы на входах и превращать пустые вводы в значения NULL, используя trim и default валидаторы ядра:

return [
    [['username', 'email'], 'trim'],
    [['username', 'email'], 'default'],
];

Вы также можете использовать более общий валидатор фильтров для более сложной фильтрации данных. Как вы можете видеть, эти правила проверки действительно не проверяют входные данные. Вместо этого они обрабатывают значения и сохраняют их обратно к проверяемым атрибутам.

Обработка пустых входов

Когда входные данные передаются из форм HTML, вам часто приходится присваивать некоторым значениям по умолчанию входы, если они пусты. Вы можете это сделать, используя валидатор по умолчанию. Например:

return [
    // set "username" and "email" as null if they are empty
    [['username', 'email'], 'default'],

    // set "level" to be 1 if it is empty
    ['level', 'default', 'value' => 1],
];

По умолчанию вход считается пустым, если его значением является пустая строка, пустой массив или null. Вы можете настроить пустую логику обнаружения по умолчанию, настроив свойство yii\validators\Validator::isEmpty() с возможностью вызова PHP. Например:

  ['agree', 'required', 'isEmpty' => function ($value) {
        return empty($value);
    }]

Специальная проверка

Иногда вам нужно выполнить специальную проверку для значений, которые не привязаны к какой-либо модели. Если вам нужно выполнить только один тип проверки (например, проверку адресов электронной почты), вы можете вызвать метод validate() нужного валидатора, например:

$email = 'test@example.com';
$validator = new yii\validators\EmailValidator();

if ($validator->validate($email, $error)) {
    echo 'Email is valid.';
} else {
    echo $error;
}

Если вам нужно выполнить несколько проверок против нескольких значений, вы можете использовать yii\base\DynamicModel, которая поддерживает объявление атрибутов и правил на лету. Его использование выглядит следующим образом:

public function actionSearch($name, $email)
{
    $model = DynamicModel::validateData(compact('name', 'email'), [
        [['name', 'email'], 'string', 'max' => 128],
        ['email', 'email'],
    ]);

    if ($model->hasErrors()) {
        // validation fails
    } else {
        // validation succeeds
    }
}

Метод yii\base\DynamicModel::validateData() создает экземпляр DynamicModel, определяет атрибуты, используя данные (name и email в этом примере), а затем вызывает yii\base\Model::validate() с заданным правилом. В качестве альтернативы вы можете использовать следующий более «классический» синтаксис для выполнения проверки данных:

public function actionSearch($name, $email)
{
    $model = new DynamicModel(compact('name', 'email'));
    $model->addRule(['name', 'email'], 'string', ['max' => 128])
        ->addRule('email', 'email')
        ->validate();

    if ($model->hasErrors()) {
        // validation fails
    } else {
        // validation succeeds
    }
}

После проверки вы можете проверить, удалось ли выполнить проверку, или нет, вызвав метод hasErrors(), а затем получить ошибки проверки из свойства errors, как это делается с обычной моделью. Вы также можете получить доступ к динамическим атрибутам, определенным через экземпляр модели, например, $model->name и $model->email.

Создание валидаторов

Помимо использования основных валидаторов, включенных в выпуски Yii, вы также можете создать свои собственные валидаторы. Вы можете создавать встроенные валидаторы или автономные валидаторы.

Встроенные валидаторы

Встроенный валидатор - это единица, определенная с точки зрения модельного метода или анонимной функции. Подпись метода/функции:

/**
 * @param string $attribute the attribute currently being validated
 * @param mixed $params the value of the "params" given in the rule
 * @param \yii\validators\InlineValidator related InlineValidator instance.
 * This parameter is available since version 2.0.11.
 */
function ($attribute, $params, $validator)

Если атрибут терпит неудачу при проверке, метод/функция должен вызвать yii\base\Model::addError(), чтобы сохранить сообщение об ошибке в модели, чтобы оно могло быть возвращено позже, чтобы представить конечным пользователям.

Ниже приведены некоторые примеры:

use yii\base\Model;

class MyForm extends Model
{
    public $country;
    public $token;

    public function rules()
    {
        return [
            // an inline validator defined as the model method validateCountry()
            ['country', 'validateCountry'],

            // an inline validator defined as an anonymous function
            ['token', function ($attribute, $params, $validator) {
                if (!ctype_alnum($this->$attribute)) {
                    $this->addError($attribute, 'The token must contain letters or digits.');
                }
            }],
        ];
    }

    public function validateCountry($attribute, $params, $validator)
    {
        if (!in_array($this->$attribute, ['USA', 'Web'])) {
            $this->addError($attribute, 'The country must be either "USA" or "Web".');
        }
    }
}

Автономные валидаторы

Автономный валидатор - это класс, расширяющий yii\validators\Validator или его дочерний класс. Вы можете реализовать свою логику валидации, переопределив метод yii\validators\Validator::validateAttribute(). Если атрибут не прошел проверку, вызовите yii\base\Model::addError(), чтобы сохранить сообщение об ошибке в модели, как это делается с встроенными валидаторами.

Например, встроенный валидатор выше может быть перемещен в новый класс [[components/validators/CountryValidator]].

namespace app\components;

use yii\validators\Validator;

class CountryValidator extends Validator
{
    public function validateAttribute($model, $attribute)
    {
        if (!in_array($model->$attribute, ['USA', 'Web'])) {
            $this->addError($model, $attribute, 'The country must be either "USA" or "Web".');
        }
    }
}

Если вы хотите, чтобы ваш валидатор поддерживал проверку значения без модели, вы также должны переопределить yii\validators\Validator::validate(). Вы также можете переопределить yii\validators\Validator::validateValue() вместо validateAttribute() и validate(), потому что по умолчанию последние два метода реализуются вызовом validateValue().

Ниже приведен пример того, как вы могли бы использовать вышеуказанный класс валидатора в вашей модели.

namespace app\models;

use Yii;
use yii\base\Model;
use app\components\validators\CountryValidator;

class EntryForm extends Model
{
    public $name;
    public $email;
    public $country;

    public function rules()
    {
        return [
            [['name', 'email'], 'required'],
            ['country', CountryValidator::className()],
            ['email', 'email'],
        ];
    }
}

Проверка множественных атрибутов

Иногда валидаторы включают несколько атрибутов. Рассмотрим следующую форму:

class MigrationForm extends \yii\base\Model
{
    /**
     * Minimal funds amount for one adult person
     */
    const MIN_ADULT_FUNDS = 3000;
    /**
     * Minimal funds amount for one child
     */
    const MIN_CHILD_FUNDS = 1500;

    public $personalSalary;
    public $spouseSalary;
    public $childrenCount;
    public $description;

    public function rules()
    {
        return [
            [['personalSalary', 'description'], 'required'],
            [['personalSalary', 'spouseSalary'], 'integer', 'min' => self::MIN_ADULT_FUNDS],
            ['childrenCount', 'integer', 'min' => 0, 'max' => 5],
            [['spouseSalary', 'childrenCount'], 'default', 'value' => 0],
            ['description', 'string'],
        ];
    }
}

Создание валидатора

Скажем, нам нужно проверить, достаточно ли семейного дохода для детей. Мы можем создать встроенный валидатор validateChildrenFunds для того, что будет работать, только когда childrenCount больше 0.

Обратите внимание, что мы не можем использовать все проверенные атрибуты (['personalSalary', 'spouseSalary', 'childrenCount']) при прикреплении валидатора. Это связано с тем, что один и тот же валидатор будет запускаться для каждого атрибута (всего три раза), и нам нужно всего один раз запустить его для всего набора атрибутов.

Вы можете использовать любой из этих атрибутов (или использовать то, что вы считаете наиболее релевантным):

['childrenCount', 'validateChildrenFunds', 'when' => function ($model) {
    return $model->childrenCount > 0;
}],

Реализация validateChildrenFunds может быть такой:

public function validateChildrenFunds($attribute, $params)
{
    $totalSalary = $this->personalSalary + $this->spouseSalary;
    // Double the minimal adult funds if spouse salary is specified
    $minAdultFunds = $this->spouseSalary ? self::MIN_ADULT_FUNDS * 2 : self::MIN_ADULT_FUNDS;
    $childFunds = $totalSalary - $minAdultFunds;
    if ($childFunds / $this->childrenCount < self::MIN_CHILD_FUNDS) {
        $this->addError('childrenCount', 'Your salary is not enough for children.');
    }
}

Вы можете игнорировать параметр $attribute, потому что проверка не связана с одним атрибутом.

Добавление ошибок

Добавление ошибки в случае нескольких атрибутов может варьироваться в зависимости от желаемой формы:

  • Выберите, по вашему мнению, наиболее релевантное поле и добавьте ошибку в его атрибут:
$this->addError('childrenCount', 'Your salary is not enough for children.');
  • Выберите несколько важных релевантных атрибутов или все атрибуты и добавьте к ним одно и то же сообщение об ошибке. Мы можем хранить сообщение в отдельной переменной перед передачей его в addError, чтобы сохранить код DRY.
$message = 'Your salary is not enough for children.';
$this->addError('personalSalary', $message);
$this->addError('wifeSalary', $message);
$this->addError('childrenCount', $message);

Или используйте цикл:

$attributes = ['personalSalary, 'wifeSalary', 'childrenCount'];
foreach ($attributes as $attribute) {
    $this->addError($attribute, 'Your salary is not enough for children.');
}
  • Добавьте общую ошибку (не связанную с конкретным атрибутом). Мы можем использовать несуществующее имя атрибута для добавления ошибки, например *, потому что существование атрибута в этой точке не проверяется.
$this->addError('*', 'Your salary is not enough for children.');

В результате мы не увидим сообщение об ошибке возле полей формы. Чтобы отобразить его, мы можем включить в отчет информацию об ошибках:

<?= $form->errorSummary($model) ?>

Проверка на стороне клиента

Проверка на стороне клиента, основанная на JavaScript, желательна, когда конечные пользователи предоставляют входные данные через формы HTML, так как это позволяет пользователям быстрее находить входные ошибки и, таким образом, обеспечивает лучший пользовательский опыт. Вы можете использовать или внедрять валидатор, который поддерживает проверку на стороне клиента в дополнение к проверке на стороне сервера.

Использование проверки на стороне клиента

Многие валидаторы ядра поддерживают проверку подлинности на клиентской стороне. Все, что вам нужно сделать, это просто использовать yii\widgets\ActiveForm для создания ваших HTML-форм. Например, LoginForm ниже объявляет два правила: один использует необходимый валидатор ядра, который поддерживается как на стороне клиента, так и на стороне сервера; Другой использует валидатор validatePassword, который поддерживается только на стороне сервера.

namespace app\models;

use yii\base\Model;
use app\models\User;

class LoginForm extends Model
{
    public $username;
    public $password;

    public function rules()
    {
        return [
            // username and password are both required
            [['username', 'password'], 'required'],

            // password is validated by validatePassword()
            ['password', 'validatePassword'],
        ];
    }

    public function validatePassword()
    {
        $user = User::findByUsername($this->username);

        if (!$user || !$user->validatePassword($this->password)) {
            $this->addError('password', 'Incorrect username or password.');
        }
    }
}

Форма HTML, созданная следующим кодом, содержит два поля ввода username и password. Если вы отправите форму без ввода чего-либо, вы обнаружите, что сообщения об ошибках, требующие ввода чего-либо, появятся сразу же без связи с сервером.

<?php $form = yii\widgets\ActiveForm::begin(); ?>
    <?= $form->field($model, 'username') ?>
    <?= $form->field($model, 'password')->passwordInput() ?>
    <?= Html::submitButton('Login') ?>
<?php yii\widgets\ActiveForm::end(); ?>

За сценой yii\widgets\ActiveForm будет читать правила проверки, объявленные в модели, и генерировать соответствующий код JavaScript для валидаторов, которые поддерживают проверку на стороне клиента. Когда пользователь изменяет значение поля ввода или отправляет форму, активируется JavaScript-код проверки на стороне клиента.

Если вы хотите полностью отключить проверку на стороне клиента, вы можете настроить свойство yii\widgets\ActiveForm::$enableClientValidation как false. Вы также можете отключить проверку на стороне клиента отдельных полей ввода, настроив их свойство yii\widgets\ActiveField::$enableClientValidation как false. Когда enableClientValidation настроен как на уровне поля ввода, так и на уровне формы, приоритет будет иметь первый.

Внедрение проверки на стороне клиента

Чтобы создать валидатор, поддерживающий проверку на стороне клиента, вы должны реализовать метод yii\validators\Validator::clientValidateAttribute(), который возвращает фрагмент кода JavaScript, который выполняет проверку на стороне клиента. В коде JavaScript вы можете использовать следующие предопределенные переменные:

  • attribute: имя проверяемого атрибута.
  • value: проверяемое значение.
  • messages: массив, используемый для хранения сообщений об ошибках проверки для этого атрибута.
  • deferred: массив, в который могут быть помещены отложенные объекты.

В следующем примере мы создаем StatusValidator, который проверяет, является ли вход действительным входом состояния в сравнении с существующими данными состояния. Валидатор поддерживает как проверку на стороне сервера, так и проверку на стороне клиента.

namespace app\components;

use yii\validators\Validator;
use app\models\Status;

class StatusValidator extends Validator
{
    public function init()
    {
        parent::init();
        $this->message = 'Invalid status input.';
    }

    public function validateAttribute($model, $attribute)
    {
        $value = $model->$attribute;
        if (!Status::find()->where(['id' => $value])->exists()) {
            $model->addError($attribute, $this->message);
        }
    }

    public function clientValidateAttribute($model, $attribute, $view)
    {
        $statuses = json_encode(Status::find()->select('id')->asArray()->column());
        $message = json_encode($this->message, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE);
        return <<<JS
if ($.inArray(value, $statuses) === -1) {
    messages.push($message);
}
JS;
    }
}

Отложенная валидация

Если вам нужно выполнить асинхронную проверку на стороне клиента, вы можете создать объекты Deferred. Например, чтобы выполнить настраиваемую проверку AJAX, вы можете использовать следующий код:

public function clientValidateAttribute($model, $attribute, $view)
{
    return <<<JS
        deferred.push($.get("/check", {value: value}).done(function(data) {
            if ('' !== data) {
                messages.push(data);
            }
        }));
JS;
}

В приведенном выше отложенная переменная предоставляется Yii, которая является массивом отложенных объектов. Метод $.get() jQuery создает объект deferred, который помещается в отложенный массив.

Вы также можете явно создать объект deferred и вызвать его метод resolve() при ударе асинхронного обратного вызова. В следующем примере показано, как проверить размеры загруженного файла изображения на стороне клиента.

public function clientValidateAttribute($model, $attribute, $view)
{
    return <<<JS
        var def = $.Deferred();
        var img = new Image();
        img.onload = function() {
            if (this.width > 150) {
                messages.push('Image too wide!!');
            }
            def.resolve();
        }
        var reader = new FileReader();
        reader.onloadend = function() {
            img.src = reader.result;
        }
        reader.readAsDataURL(file);

        deferred.push(def);
JS;
}

Для простоты deferred массив снабжен специальным методом add(), который автоматически создает объект deferred и добавляет его в отложенный массив. Используя этот метод, вы можете упростить приведенный выше пример следующим образом:

public function clientValidateAttribute($model, $attribute, $view)
{
    return <<<JS
        deferred.add(function(def) {
            var img = new Image();
            img.onload = function() {
                if (this.width > 150) {
                    messages.push('Image too wide!!');
                }
                def.resolve();
            }
            var reader = new FileReader();
            reader.onloadend = function() {
                img.src = reader.result;
            }
            reader.readAsDataURL(file);
        });
JS;
}

Проверка AJAX

Некоторые проверки могут быть выполнены только на стороне сервера, потому что только сервер имеет необходимую информацию. Например, чтобы проверить, является ли имя пользователя уникальным или нет, необходимо проверить таблицу пользователей на стороне сервера. В этом случае вы можете использовать валидацию на основе AJAX. Это вызовет AJAX-запрос в фоновом режиме для проверки ввода, сохраняя тот же пользовательский опыт, что и обычная проверка на стороне клиента.

Чтобы включить проверку AJAX для одного поля ввода, настройте для свойства enableAjaxValidation этого поля значение true и укажите id формы:

use yii\widgets\ActiveForm;

$form = ActiveForm::begin([
    'id' => 'registration-form',
]);

echo $form->field($model, 'username', ['enableAjaxValidation' => true]);

// ...

ActiveForm::end();

Чтобы включить проверку AJAX для всей формы, настройте enableAjaxValidation на уровне формы:

$form = ActiveForm::begin([
    'id' => 'contact-form',
    'enableAjaxValidation' => true,
]);

Вам также необходимо подготовить сервер, чтобы он мог обрабатывать запросы проверки AJAX. Этого можно добиться с помощью фрагмента кода, подобного следующему в действиях контроллера:

if (Yii::$app->request->isAjax && $model->load(Yii::$app->request->post())) {
    Yii::$app->response->format = Response::FORMAT_JSON;
    return ActiveForm::validate($model);
}

Вышеприведенный код проверит, является ли текущий запрос AJAX. Если да, то он ответит на этот запрос, выполнив проверку и возвратив ошибки в формате JSON. Если для обоих enableClientValidation и enableAjaxValidation установлено значение true, запрос проверки AJAX будет запущен только после успешной проверки клиента.