2 关注者

验证输入

根据经验法则,您永远不应该信任从最终用户接收到的数据,并且在将其用于任何用途之前,都应该始终对其进行验证。

给定一个 模型,其中填充了用户输入,您可以通过调用 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', ...
]

对于每个规则,您必须至少指定规则应用于哪些属性以及规则的类型。您可以通过以下其中一种形式指定规则类型

  • 核心验证器的别名,例如 requiredindate 等。有关核心验证器的完整列表,请参阅 核心验证器
  • 模型类中的验证方法名称或匿名函数。有关更多详细信息,请参阅 内联验证器 小节。
  • 完整限定的验证器类名。有关更多详细信息,请参阅 独立验证器 小节。

规则可用于验证一个或多个属性,并且一个属性可以由一个或多个规则验证。可以通过指定 on 选项仅在某些 场景 中应用规则。如果您未指定 on 选项,则表示该规则将应用于所有场景。

当调用 validate() 方法时,它会执行以下步骤来执行验证

  1. 通过使用当前 场景yii\base\Model::scenarios() 获取属性列表来确定应验证哪些属性。这些属性称为活动属性
  2. 通过使用当前 场景yii\base\Model::rules() 获取规则列表来确定应使用哪些验证规则。这些规则称为活动规则
  3. 使用每个活动规则来验证与该规则关联的每个活动属性。验证规则按其列出的顺序进行评估。

根据以上验证步骤,一个属性只有在以下情况下才会被验证:它是一个在scenarios()中声明的活动属性,并且与一个或多个在rules()中声明的活动规则相关联。

注意:为规则命名非常方便,例如:

public function rules()
{
    return [
        // ...
        'password' => [['password'], 'string', 'max' => 60],
    ];
}

你可以在子模型中使用它

public function rules()
{
    $rules = parent::rules();
    unset($rules['password']);
    return $rules;
}

自定义错误消息

大多数验证器都有默认的错误消息,当模型的属性验证失败时,这些消息将被添加到正在验证的模型中。例如,required 验证器会在模型的username属性使用此验证器失败时,向模型添加一条消息“用户名不能为空”。

你可以通过在声明规则时指定message属性来自定义规则的错误消息,如下所示:

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

一些验证器可能支持额外的错误消息,以便更精确地描述验证失败的不同原因。例如,number 验证器支持 tooBigtooSmall 来描述当被验证的值过大和过小时的验证失败。你可以像在验证规则中配置验证器的其他属性一样配置这些错误消息。

验证事件

当调用 yii\base\Model::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输入周围的空格。你可以使用验证规则来实现此目标。

以下示例显示了如何使用 trimdefault 核心验证器来修剪输入中的空格并将空输入转换为 null。

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

你也可以使用更通用的 filter 验证器来执行更复杂的数据过滤。

如你所见,这些验证规则并没有真正验证输入。相反,它们将处理值并将它们保存回正在验证的属性。

用户输入的完整处理过程在以下示例代码中显示,该代码将确保仅整数值存储在属性中

['age', 'trim'],
['age', 'default', 'value' => null],
['age', 'integer', 'min' => 0],
['age', 'filter', 'filter' => 'intval', 'skipOnEmpty' => true],

以上代码将对输入执行以下操作

  1. 修剪输入值的空白字符。
  2. 确保空输入在数据库中存储为null;我们区分“未设置”的值和实际值0。如果不允许null,则可以在此处设置其他默认值。
  3. 如果输入不为空,则验证该值是否为大于 0 的整数。普通验证器将 $skipOnEmpty 设置为true
  4. 确保值的数据类型为整数,例如将字符串'42'转换为整数42。在这里,我们将 $skipOnEmpty 设置为true,而在 filter 验证器上默认为false

处理空输入

当从 HTML 表单提交输入数据时,如果输入为空,你通常需要为其分配一些默认值。你可以使用 default 验证器来做到这一点。例如:

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,则认为输入为空。你可以通过使用 PHP 可调用对象配置 yii\validators\Validator::isEmpty() 属性来自定义默认的空检测逻辑。例如:

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

注意:如果大多数验证器的 yii\validators\Validator::$skipOnEmpty 属性取默认值true,则它们不会处理空输入。如果其关联的属性接收空输入,它们将简单地跳过验证过程。在 核心验证器 中,只有captchadefaultfilterrequiredtrim验证器会处理空输入。

临时验证

有时你需要对不绑定到任何模型的值进行临时验证

如果你只需要执行一种类型的验证(例如验证电子邮件地址),你可以调用所需验证器的 validate() 方法,如下所示

$email = '[email protected]';
$validator = new yii\validators\EmailValidator();

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

注意:并非所有验证器都支持这种类型的验证。例如,unique 核心验证器旨在仅与模型一起使用。

注意:yii\base\Validator::skipOnEmpty 属性仅用于 yii\base\Model 验证。在没有模型的情况下使用它无效。

如果你需要对多个值执行多个验证,可以使用 yii\base\DynamicModel,它支持动态声明属性和规则。其用法如下:

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

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

yii\base\DynamicModel::validateData() 方法创建了一个DynamicModel实例,使用给定的数据(此示例中的nameemail)定义属性,然后使用给定的规则调用 yii\base\Model::validate()

或者,你可以使用以下更“经典”的语法来执行临时数据验证

public function actionSearch($name, $email)
{
    $model = new DynamicModel(['name' => $name, 'email' => $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 $validator related InlineValidator instance.
 * This parameter is available since version 2.0.11.
 * @param mixed $current the currently validated value of attribute.
 * This parameter is available since version 2.0.36.
 */
function ($attribute, $params, $validator, $current)

如果属性验证失败,方法/函数应该调用 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', 'Indonesia'])) {
            $this->addError($attribute, 'The country must be either "USA" or "Indonesia".');
        }
    }
}

注意:从 2.0.11 版本开始,你可以使用 yii\validators\InlineValidator::addError() 添加错误。这样,就可以立即使用 yii\i18n\I18N::format() 格式化错误消息。在错误消息中使用{attribute}{value}分别引用属性标签(无需手动获取)和属性值。

$validator->addError($this, $attribute, 'The value "{value}" is not acceptable for {attribute}.');

注意:默认情况下,如果其关联的属性接收空输入或已经失败了一些验证规则,则不会应用内联验证器。如果你想确保始终应用规则,则可以在规则声明中将 skipOnEmpty 和/或 skipOnError 属性配置为false。例如

[
    ['country', 'validateCountry', 'skipOnEmpty' => false, 'skipOnError' => false],
]

独立验证器

独立验证器是一个扩展 yii\validators\Validator 或其子类的类。你可以通过重写 yii\validators\Validator::validateAttribute() 方法来实现其验证逻辑。如果属性验证失败,请调用 yii\base\Model::addError() 将错误消息保存到模型中,就像你对 内联验证器 所做的那样。

例如,以上内联验证器可以移动到新的 [[components/validators/CountryValidator]] 类中。在这种情况下,我们可以使用 yii\validators\Validator::addError() 为模型设置自定义消息。

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, 'The country must be either "{country1}" or "{country2}".', ['country1' => 'USA', 'country2' => 'Indonesia']);
        }
    }
}

如果你希望你的验证器支持在没有模型的情况下验证值,则还应该重写 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::class],
            ['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'])。这是因为同一个验证器将为每个属性运行(总共 3 次),而我们只需要为整个属性集运行一次。

你可以使用任何这些属性(或使用你认为最相关的属性)

['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之前将消息存储在单独的变量中,以保持代码简洁。
$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) ?>

注意:创建同时验证多个属性的验证器在 社区食谱 中有详细描述。

客户端验证

当最终用户通过 HTML 表单提供输入时,基于 JavaScript 的客户端验证非常理想,因为它允许用户更快地发现输入错误,从而提供更好的用户体验。除了服务器端验证之外,您还可以使用或实现支持客户端验证的验证器。

信息:虽然客户端验证是理想的,但它不是必须的。其主要目的是为用户提供更好的体验。与来自最终用户的输入数据类似,您永远不应该信任客户端验证。因此,您应该始终通过调用 yii\base\Model::validate() 执行服务器端验证,如前面小节所述。

使用客户端验证

许多 核心验证器 原生支持客户端验证。您只需使用 yii\widgets\ActiveForm 构建 HTML 表单即可。例如,下面的 LoginForm 声明了两个规则:一个使用 required 核心验证器,该验证器在客户端和服务器端都受支持;另一个使用 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 表单包含两个输入字段 usernamepassword。如果您在不输入任何内容的情况下提交表单,您会发现要求您输入内容的错误消息会立即出现,而无需与服务器进行任何通信。

<?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 时,前者将优先。

信息:从 2.0.11 版本开始,所有扩展自 yii\validators\Validator 的验证器都从单独的方法 - yii\validators\Validator::getClientOptions() 接收客户端选项。您可以使用它

  • 如果您想实现自己的自定义客户端验证,但保留与服务器端验证器选项的同步;
  • 扩展或自定义以适合您的特定需求
public function getClientOptions($model, $attribute)
{
    $options = parent::getClientOptions($model, $attribute);
    // Modify $options here

    return $options;
}

实现客户端验证

要创建支持客户端验证的验证器,您应该实现 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;
    }
}

提示:以上代码主要是为了演示如何支持客户端验证。在实践中,您可以使用 in 核心验证器来实现相同目标。您可以像下面这样编写验证规则

[
    ['status', 'in', 'range' => Status::find()->select('id')->asArray()->column()],
]

提示:如果您需要手动处理客户端验证,例如动态添加字段或执行一些自定义 UI 逻辑,请参阅 Yii 2.0 Cookbook 中的 通过 JavaScript 使用 ActiveForm

延迟验证

如果您需要执行异步客户端验证,则可以创建 延迟对象。例如,要执行自定义 AJAX 验证,您可以使用以下代码

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

在上面,deferred 变量由 Yii 提供,它是一个延迟对象的数组。$.get() jQuery 方法创建了一个延迟对象,该对象被推送到 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;
}

注意:必须在验证属性后调用 resolve() 方法。否则,主表单验证将无法完成。

为简单起见,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 配置为 true

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

注意:当在输入字段级别和表单级别都配置 enableAjaxValidation 属性时,前者将优先。

您还需要准备服务器,以便它可以处理 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 格式返回错误来响应此请求。

信息:您还可以使用 延迟验证 执行 AJAX 验证。但是,此处描述的 AJAX 验证功能更系统化,并且需要更少的编码工作。

enableClientValidationenableAjaxValidation 都设置为 true 时,只有在客户端验证成功后才会触发 AJAX 验证请求。请注意,如果验证单个字段(如果 validateOnChangevalidateOnBlurvalidateOnType 设置为 true),则当该字段单独成功通过客户端验证时,将发送 AJAX 请求。

发现错别字或您认为此页面需要改进?
在 github 上编辑它 !