输入验证
要给 model 填充其所需的用户输入数据,可以调用 yii\base\Model::validate() 方法验证输入的数据,该方法会返回一个布尔值,表示是否通过验证。若没有通过,可以通过 yii\base\Model::$errors 属性获取相应的报错信息。
$model = new \app\models\UserForm();
// 根据用户的输入填充到模型的属性中
$model->load(\Yii::$app->request->post());
// 等效于下面这样:
// $model->attributes = \Yii::$app->request->post('UserForm');
if ($model->validate()) {
// 所有输入通过验证
} else {
// 验证失败: $errors 是一个包含错误信息的数组
$errors = $model->errors;
}
定义验证规则
要让 validate() 方法起作用,需要声明与需验证模型特性相关的验证规则。 因此需要重写 yii\base\Model::rules() 方法
public function rules(): array
{
return [
[['username', 'auth_key', 'password_hash', 'email'], 'required'],
[['username'], 'string', 'max' => 32],
[['email'], 'string', 'max' => 255],
[['email'], 'email'],
];
}
rules() 方法返回一个由规则所组成的数组,每一个规则都是如下格式的小数组。一个规则可用于验证一个或多个模型特性,且一个特性可以被一个或多个规则所验证。 一个规则可以用于特定场景(scenario),只要指定 on 选项。如果不指定 on 选项,则该规则会适配于所有场景。
[
// 必填,需要验证的特性
// 如果是一个属性,可以直接写属性的名字,不必写在数组里
['attribute1', 'attribute2', ...],
// 必填,规则的类型。
// 可以是一个类名,验证器别名,或者是一个验证器方法名
'validator',
// 可选,指出这个规则在哪些场景下生效
// 如果没有给出,意味着这个规则在所有场景都生效
// 如果希望在所有场景下生效,但是在排除的场景里不生效,可以配置 "except" 选项
'on' => ['scenario1', 'scenario2', ...],
// 可选,为验证器对象指定额外的配置
'property1' => 'value1', 'property2' => 'value2', ...
]
每个规则,至少需要指定该规则要验证的属性和规则的类型,规则类型有以下几种
- 核心验证器,例如:required、string、in、date等。(核心验证器)
- 模型类中的某个验证方法的名称,或一个匿名方法。(行内验证器)
- 验证器的名称。(独立验证器)
当调用 validate() 方法时,验证步骤如下:
- 从 yii\base\Model::scenarios() 方法中筛选出当前场景的信息,筛选出的这些特性被称为激活特性,即是要验证的特性
- 从yii\base\Model::rules() 方法中筛选出适用于当前场景的规则, 筛选出的这些规则被称为激活规则。即是要验证的规则
- 用每个激活规则去验证对应的激活特性。
基于以上验证步骤,有且仅有声明在 scenarios() 方法里的激活特性,且它还必须与一或多个声明自 rules() 里的激活规则相关联才会被验证。
自定义错误信息
大多数的验证器都有默认的错误信息,当模型的某个特性验证失败时,该错误信息会被返回给模型。 比如,用 required 验证器的规则检验 username 特性失败的话,会返还给模型 “Username cannot be blank.” 信息。
因此,可以在声明规则的同时定义message属性的值为规则的错误信息
public function rules()
{
return [
['username', 'required', 'message' => 'Please choose a username.'],
];
}
验证事件
当调用 yii\base\Model::validate() 方法验证规则时,在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 callable 函数对象:
/**
* @param Model $model 要验证的模型对象
* @param string $attribute 待测特性名
* @return bool 返回是否启用该规则
*/
function ($model, $attribute)
若需要支持客户端的条件验证,需要配置whenClient 属性, 它会读入一条包含有 JavaScript 函数的字符串。 这个函数将被用于确定该客户端验证规则是否被启用。比如,
['state', 'required', 'when' => function ($model) {
return $model->country == 'USA';
}, 'whenClient' => "function (attribute, value) {
return $('#country').value == 'USA';
}"
]
数据预处理
用户输入的数据经常需要进行数据过滤,或者叫预处理。比如你可能会去掉用户输入的username值前后的空格,用验证规则可以实现该操作
return [
[['username', 'email'], 'trim'],
[['username', 'email'], 'default'],
];
这些验证规则并不真的对输入数据进行任何验证。而是对输入数据进行一些处理, 然后把它们存回当前被验证的模型特性。
下面的代码示例展示对用户输入的数据进行完整处理, 这将确保只将整数值存储在一个属性中:
['age', 'trim'],
['age', 'default', 'value' => null],
['age', 'integer', 'integerOnly' => true, 'min' => 0],
['age', 'filter', 'filter' => 'intval', 'skipOnEmpty' => true],
以上代码对输入的数据执行以下的操作:
去掉输入数据的前后空格
确保空输入在数据库中存储为 null;我们区分未设置值和实际值为0之间的区别。
如果值不允许为 null,则可以在此处设置另一个默认值。
如果该值不为空,则验证该值是否为大于0的整数。大多数验证器的 $skipOnEmpty 属性默认为true。
确保该值为整数类型,例如将字符串 ‘42’ 转换为整数 42。 s k i p O n E m p t y 设置为 t r u e 是因为 f i l t e r 验证器的 skipOnEmpty设置为 true 是因为filter验证器的 skipOnEmpty设置为true是因为filter验证器的skipOnEmpty 值为false
处理空输入
当输入数据是通过 HTML 表单提交,我们经常需要给空的输入项赋默认值。通过调整 default 验证器可以实现给空输入赋默认值。
举例来说:
return [
// 若 "username" 和 "email" 为空,则设为 null
[['username', 'email'], 'default'],
// 若 "level" 为空,则设其为 1
['level', 'default', 'value' => 1],
];
默认情况下,当输入项为空字符串,空数组,或 null 时,会被视为“空值”。 可以通过配置yii\validators\Validator::isEmpty() 属性来自定义空值的判定规则。比如,
['agree', 'required', 'isEmpty' => function ($value) {
return empty($value);
}
]
临时验证
有时,我们需要对某些没有绑定任何模型类的值进行 临时验证。若只需要进行一种类型的验证 (e.g. 验证邮箱地址),可以调用所需验证器的 validate() 方法。
$email = 'test@example.com';
$validator = new yii\validators\EmailValidator();
if ($validator->validate($email, $error)) {
echo '有效的 Email 地址。';
} 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()) {
// 验证失败
} else {
// 验证成功
}
}
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()) {
// 验证失败
} else {
// 验证成功
}
}
验证之后可以通过调用 hasErrors() 方法来检查验证通过与否,并通过 errors 属性获得验证的错误信息,过程与普通模型类一致。 也可以访问模型对象内定义的动态特性,就像: $model->name 和 $model->email。
创建验证器
除了使用 Yii 的发布版里所包含的核心验证器之外,我们也可以创建自己的验证器。 自定义的验证器可以是行内验证器,也可以是独立验证器
行内验证器
行内验证器是一种以模型方法或匿名函数的形式定义的验证器。 这些方法/函数的结构如下:
/**
* @param string $attribute 当前被验证的特性
* @param array $params 以名-值对形式提供的额外参数
* @param \yii\validators\InlineValidator $validator 相关的 InlineValidator 实例。
* 此参数自版本 2.0.11 起可用。
*/
function ($attribute, $params)
若某特性的验证失败了,该方法/函数应该调用 yii\base\Model::addError() 保存错误信息到模型内。 这样这些错误就能在之后的操作中,被读取并展现给终端用户。
use yii\base\Model;
class MyForm extends Model
{
public $country;
public $token;
public function rules()
{
return [
// 定义为模型方法 validateCountry() 的行内验证器
['country', 'validateCountry'],
// 定义为匿名函数的行内验证器
['token', function ($attribute, $params) {
if (!ctype_alnum($this->$attribute)) {
$this->addError($attribute, 'The token must contain letters or digits.');
}
}],
];
}
public function validateCountry($attribute, $params)
{
if (!in_array($this->$attribute, ['USA', 'Web'])) {
$this->addError($attribute, 'The country must be either "USA" or "Web".');
}
}
}
从 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() 以保存错误信息到模型内, 操作与 inline validators 所需操作完全一样
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".');
}
}
}
若我们想要验证器支持不使用 model 的数据验证,我们还应该重写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
{
/**
* 一个成年人的最少花销
*/
const MIN_ADULT_FUNDS = 3000;
/**
* 一个孩子的最小花销
*/
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;
}],
//可以忽略 $attribute 参数,因为这个验证过程不仅仅关联一个属性
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.');
}
}
添加错误信息
在添加错误信息的时候,如果是多个属性,可以根据自己想要的格式使用多种情况:
- 选择一个你认为最相关的字段把错误信息添加到它的属性里:
$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\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 和 password 都是必填项
[['username', 'password'], 'required'],
// password 用 validatePassword() 方法验证
['password', 'validatePassword'],
];
}
public function validatePassword()
{
$user = User::findByUsername($this->username);
if (!$user || !$user->validatePassword($this->password)) {
$this->addError('password', 'Incorrect username or password.');
}
}
}
下面的代码构建了包含 username 和 password 两个表单项的 HTML 表单。 如果不输入任何内容直接提交表单,就会发现提示输入内容的错误信息立刻出现, 而这并没有和服务端交互。
<?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::KaTeX parse error: Undefined control sequence: \widgets at position 49: …。也可以通过设置它们的 yii\̲w̲i̲d̲g̲e̲t̲s̲\ActiveField::enableClientValidation 属性为 false 来单独关闭某一个表单项。 当在表单项级别和表单级别都设置了 enableClientValidation 的时候, 前者(表单项)的级别优先生效。
实现客户端验证
为了创建一个支持客户端验证的验证器,应该实现 yii\validators\Validator::clientValidateAttribute() 方法,该方法返回一段 JavaScript 代码用来在客户端执行验证。在这段 JavaScript 代码里,可以使用下面几个预定义的变量:
attribute
:被验证的属性名。value
:被验证的值。messages
:一个给属性保存验证错误信息的数组。deferred
:一个支持添加 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()], ]
deferred 验证
如果需要执行异步客户端验证,可以创建 Deferred objects。 比如要执行一段自定义的 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 提供的,它是一个 Deferred 对象的数组。这个 $.get() jQuery 方法用来产生一个 Deferred 对象然后推送到 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;
}
resolve()
方法必须在所有属性都验证完之后调用。不然表单不会完成整体的验证流程。
为了简单起见,deferred 数组封装了一个快捷方法 add(),它可以自动创建 Deferred 对象然后把它添加到 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 格式的错误信息来响应这次请求。
当
enableClientValidation
和enableAjaxValidation
都设置为true
时,只有客户端验证成功之后才会触发 AJAX 的验证请求。如果验证某个表单项的时候凑巧validateOnChange
,validateOnBlur
或者validateOnType
其中之一设置了true
,那么这个表单项在单独通过这样的客户端验证时, 也会发起 AJAX 请求。