一般說來,程序猿永遠不應該信任從最終用戶直接接收到的數(shù)據(jù),并且使用它們之前應始終先驗證其可靠性。
要給?model?填充其所需的用戶輸入數(shù)據(jù),你可以調(diào)用 yii\base\Model::validate() 方法驗證它們。該方法會返回一個布爾值,指明是否通過驗證。若沒有通過,你能通過 yii\base\Model::errors 屬性獲取相應的報錯信息。比如,
$model = new \app\models\ContactForm;
// 用用戶輸入來填充模型的特性
$model->attributes = \Yii::$app->request->post('ContactForm');
if ($model->validate()) {
// 若所有輸入都是有效的
} else {
// 有效性驗證失敗:$errors 屬性就是存儲錯誤信息的數(shù)組
$errors = $model->errors;
}
validate()
?方法,在幕后為執(zhí)行驗證操作,進行了以下步驟:
要讓?validate()
?方法起作用,你需要聲明與需驗證模型特性相關(guān)的驗證規(guī)則。為此,需要重寫 yii\base\Model::rules() 方法。下面的例子展示了如何聲明用于驗證?ContactForm
?模型的相關(guān)驗證規(guī)則:
public function rules()
{
return [
// name,email,subject 和 body 特性是 `require`(必填)的
[['name', 'email', 'subject', 'body'], 'required'],
// email 特性必須是一個有效的 email 地址
['email', 'email'],
];
}
yii\base\Model::rules() 方法應返回一個由規(guī)則所組成的數(shù)組,每一個規(guī)則都呈現(xiàn)為以下這類格式的小數(shù)組:
[
// 必須項,用于指定那些模型特性需要通過此規(guī)則的驗證。
// 對于只有一個特性的情況,可以直接寫特性名,而不必用數(shù)組包裹。
['attribute1', 'attribute2', ...],
// 必填項,用于指定規(guī)則的類型。
// 它可以是類名,驗證器昵稱,或者是驗證方法的名稱。
'validator',
// 可選項,用于指定在場景(scenario)中,需要啟用該規(guī)則
// 若不提供,則代表該規(guī)則適用于所有場景
// 若你需要提供除了某些特定場景以外的所有其他場景,你也可以配置 "except" 選項
'on' => ['scenario1', 'scenario2', ...],
// 可選項,用于指定對該驗證器對象的其他配置選項
'property1' => 'value1', 'property2' => 'value2', ...
]
對于每個規(guī)則,你至少需要指定該規(guī)則適用于哪些特性,以及本規(guī)則的類型是什么。你可以指定以下的規(guī)則類型之一:
required
、in
、date
,等等。請參考核心驗證器章節(jié)查看完整的核心驗證器列表。一個規(guī)則可用于驗證一個或多個模型特性,且一個特性可以被一個或多個規(guī)則所驗證。一個規(guī)則可以施用于特定場景(scenario),只要指定?on
?選項。如果你不指定?on
?選項,那么該規(guī)則會適配于所有場景。
當調(diào)用?validate()
?方法時,它將運行以下幾個具體的驗證步驟:
基于以上驗證步驟,有且僅有聲明在?scenarios()
?方法里的激活特性,且它還必須與一或多個聲明自?rules()
?里的激活規(guī)則相關(guān)聯(lián)才會被驗證。
大多數(shù)的驗證器都有默認的錯誤信息,當模型的某個特性驗證失敗的時候,該錯誤信息會被返回給模型。比如,用 yii\validators\RequiredValidator 驗證器的規(guī)則檢驗?username
?特性失敗的話,會返還給模型 "Username cannot be blank." 信息。
你可以通過在聲明規(guī)則的時候同時指定?message
?屬性,來定制某個規(guī)則的錯誤信息,比如這樣:
public function rules()
{
return [
['username', 'required', 'message' => 'Please choose a username.'],
];
}
一些驗證器還支持用于針對不同原因的驗證失敗返回更加準確的額外錯誤信息。比如,yii\validators\NumberValidator 驗證器就支持 yii\validators\NumberValidator::tooBig 和 yii\validators\NumberValidator::tooSmall 兩種錯誤消息用于分別返回輸入值是太大還是太小。 你也可以像配置驗證器的其他屬性一樣配置它們倆各自的錯誤信息。
當調(diào)用 yii\base\Model::validate() 方法的過程里,它同時會調(diào)用兩個特殊的方法,把它們重寫掉可以實現(xiàn)自定義驗證過程的目的:
若要只在某些條件滿足時,才驗證相關(guān)特性,比如:是否驗證某特性取決于另一特性的值,你可以通過 yii\validators\Validator::when 屬性來定義相關(guān)條件。舉例而言,
[
['state', 'required', 'when' => function($model) {
return $model->country == 'USA';
}],
]
yii\validators\Validator::when 屬性會讀入一個如下所示結(jié)構(gòu)的 PHP callable 函數(shù)對象:
/**
* @param Model $model 要驗證的模型對象
* @param string $attribute 待測特性名
* @return boolean 返回是否啟用該規(guī)則
*/
function ($model, $attribute)
若你需要支持客戶端的條件驗證,你應該配置 yii\validators\Validator::whenClient 屬性,它會讀入一條包含有 JavaScript 函數(shù)的字符串。這個函數(shù)將被用于確定該客戶端驗證規(guī)則是否被啟用。比如,
[
['state', 'required', 'when' => function ($model) {
return $model->country == 'USA';
}, 'whenClient' => "function (attribute, value) {
return $('#country').value == 'USA';
}"],
]
用戶輸入經(jīng)常需要進行數(shù)據(jù)過濾,或者叫預處理。比如你可能會需要先去掉?username
?輸入的收尾空格。你可以通過使用驗證規(guī)則來實現(xiàn)此目的。
下面的例子展示了如何去掉輸入信息的首尾空格,并將空輸入返回為 null。具體方法為通過調(diào)用?trim?和?default?核心驗證器:
[
[['username', 'email'], 'trim'],
[['username', 'email'], 'default'],
]
也還可以用更加通用的?filter(濾鏡)?核心驗證器來執(zhí)行更加復雜的數(shù)據(jù)過濾。
如你所見,這些驗證規(guī)則并不真的對輸入數(shù)據(jù)進行任何驗證。而是,對輸入數(shù)據(jù)進行一些處理,然后把它們存回當前被驗證的模型特性。
當輸入數(shù)據(jù)是通過 HTML 表單,你經(jīng)常會需要給空的輸入項賦默認值。你可以通過調(diào)整?default?驗證器來實現(xiàn)這一點。舉例來說,
[
// 若 "username" 和 "email" 為空,則設為 null
[['username', 'email'], 'default'],
// 若 "level" 為空,則設其為 1
['level', 'default', 'value' => 1],
]
默認情況下,當輸入項為空字符串,空數(shù)組,或 null 時,會被視為“空值”。你也可以通過配置 yii\validators\Validator::isEmpty 屬性來自定義空值的判定規(guī)則。比如,
[
['agree', 'required', 'isEmpty' => function ($value) {
return empty($value);
}],
]
注意:對于絕大多數(shù)驗證器而言,若其 yii\base\Validator::skipOnEmpty 屬性為默認值 true,則它們不會對空值進行任何處理。也就是當他們的關(guān)聯(lián)特性接收到空值時,相關(guān)驗證會被直接略過。在?核心驗證器?之中,只有?
captcha
(驗證碼),default
(默認值),filter
(濾鏡),required
(必填),以及?trim
(去首尾空格),這幾個驗證器會處理空輸入。
有時,你需要對某些沒有綁定任何模型類的值進行?臨時驗證。
若你只需要進行一種類型的驗證 (e.g. 驗證郵箱地址),你可以調(diào)用所需驗證器的 yii\validators\Validator::validate() 方法。像這樣:
$email = 'test@example.com';
$validator = new yii\validators\EmailValidator();
if ($validator->validate($email, $error)) {
echo '有效的 Email 地址。';
} else {
echo $error;
}
注意:不是所有的驗證器都支持這種形式的驗證。比如?unique(唯一性)核心驗證器就就是一個例子,它的設計初衷就是只作用于模型類內(nèi)部的。
若你需要針對一系列值執(zhí)行多項驗證,你可以使用 yii\base\DynamicModel 。它支持即時添加特性和驗證規(guī)則的定義。它的使用規(guī)則是這樣的:
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() 方法會創(chuàng)建一個?DynamicModel
?的實例對象,并通過給定數(shù)據(jù)定義模型特性(以?name
?和email
?為例),之后用給定規(guī)則調(diào)用 yii\base\Model::validate() 方法。
除此之外呢,你也可以用如下的更加“傳統(tǒng)”的語法來執(zhí)行臨時數(shù)據(jù)驗證:
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 {
// 驗證成功
}
}
驗證之后你可以通過調(diào)用 yii\base\DynamicModel::hasErrors() 方法來檢查驗證通過與否,并通過 yii\base\DynamicModel::errors 屬性獲得驗證的錯誤信息,過程與普通模型類一致。你也可以訪問模型對象內(nèi)定義的動態(tài)特性,就像:?$model->name
?和?$model->email
。
除了使用 Yii 的發(fā)布版里所包含的核心驗證器之外,你也可以創(chuàng)建你自己的驗證器。自定義的驗證器可以是行內(nèi)驗證器,也可以是獨立驗證器。
行內(nèi)驗證器是一種以模型方法或匿名函數(shù)的形式定義的驗證器。這些方法/函數(shù)的結(jié)構(gòu)如下:
/**
* @param string $attribute 當前被驗證的特性
* @param array $params 以名-值對形式提供的額外參數(shù)
*/
function ($attribute, $params)
若某特性的驗證失敗了,該方法/函數(shù)應該調(diào)用 yii\base\Model::addError() 保存錯誤信息到模型內(nèi)。這樣這些錯誤就能在之后的操作中,被讀取并展現(xiàn)給終端用戶。
下面是一些例子:
use yii\base\Model;
class MyForm extends Model
{
public $country;
public $token;
public function rules()
{
return [
// 以模型方法 validateCountry() 形式定義的行內(nèi)驗證器
['country', 'validateCountry'],
// 以匿名函數(shù)形式定義的行內(nèi)驗證器
['token', function ($attribute, $params) {
if (!ctype_alnum($this->$attribute)) {
$this->addError($attribute, 'token 本身必須包含字母或數(shù)字。');
}
}],
];
}
public function validateCountry($attribute, $params)
{
if (!in_array($this->$attribute, ['USA', 'Web'])) {
$this->addError($attribute, 'The country must be either "USA" or "Web".');
}
}
}
注意:缺省狀態(tài)下,行內(nèi)驗證器不會在關(guān)聯(lián)特性的輸入值為空或該特性已經(jīng)在其他驗證中失敗的情況下起效。若你想要確保該驗證器始終啟用的話,你可以在定義規(guī)則時,酌情將 yii\validators\Validator::skipOnEmpty 以及 yii\validators\Validator::skipOnError 屬性設為 false,比如,?`````php [
['country', 'validateCountry', 'skipOnEmpty' => false, 'skipOnError' => false],
]?`````
獨立驗證器是繼承自 yii\validators\Validator 或其子類的類。你可以通過重寫 yii\validators\Validator::validateAttribute() 來實現(xiàn)它的驗證規(guī)則。若特性驗證失敗,可以調(diào)用 yii\base\Model::addError() 以保存錯誤信息到模型內(nèi),操作與?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($attribute, 'The country must be either "USA" or "Web".');
}
}
}
若你想要驗證器支持不使用 model 的數(shù)據(jù)驗證,你還應該重寫 yii\validators\Validator::validate() 方法。你也可以通過重寫 yii\validators\Validator::validateValue() 方法替代?validateAttribute()
?和?validate()
,因為默認狀態(tài)下,后兩者的實現(xiàn)使用過調(diào)用?validateValue()
實現(xiàn)的。
當終端用戶通過 HTML 表單提供相關(guān)輸入信息時,我們可能會需要用到基于 JavaScript 的客戶端驗證。因為,它可以讓用戶更快速的得到錯誤信息,也因此可以提供更好的用戶體驗。你可以使用或自己實現(xiàn)除服務器端驗證之外,還能額外客戶端驗證功能的驗證器。
補充:盡管客戶端驗證為加分項,但它不是必須項。它存在的主要意義在于給用戶提供更好的客戶體驗。正如“永遠不要相信來自終端用戶的輸入信息”,也同樣永遠不要相信客戶端驗證?;谶@個理由,你應該始終如前文所描述的那樣,通過調(diào)用 yii\base\Model::validate() 方法執(zhí)行服務器端驗證。
許多核心驗證器都支持開箱即用的客戶端驗證。你只需要用 yii\widgets\ActiveForm 的方式構(gòu)建 HTML 表單即可。比如,下面的LoginForm
(登錄表單)聲明了兩個規(guī)則:其一為?required?核心驗證器,它同時支持客戶端與服務器端的驗證;另一個則采用validatePassword
?行內(nèi)驗證器,它只支持服務器端。
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'],
// 用 validatePassword() 驗證 password
['password', 'validatePassword'],
];
}
public function validatePassword()
{
$user = User::findByUsername($this->username);
if (!$user || !$user->validatePassword($this->password)) {
$this->addError('password', 'Incorrect username or password.');
}
}
}
使用如下代碼構(gòu)建的 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 會讀取聲明在模型類中的驗證規(guī)則,并生成那些支持支持客戶端驗證的驗證器所需的 JavaScript 代碼。當用戶修改輸入框的值,或者提交表單時,就會觸發(fā)相應的客戶端驗證 JS 代碼。
若你需要完全關(guān)閉客戶端驗證,你只需配置 yii\widgets\ActiveForm::enableClientValidation 屬性為 false。你同樣可以關(guān)閉各個輸入框各自的客戶端驗證,只要把它們的 yii\widgets\ActiveField::enableClientValidation 屬性設為 false。
要穿件一個支持客戶端驗證的驗證器,你需要實現(xiàn) yii\validators\Validator::clientValidateAttribute() 方法,用于返回一段用于運行客戶端驗證的 JavaScript 代碼。在這段 JavaScript 代碼中,你可以使用以下預定義的變量:
attribute
:正在被驗證的模型特性的名稱。value
:進行驗證的值。messages
:一個用于暫存模型特性的報錯信息的數(shù)組。在下面的例子里,我們會創(chuàng)建一個?StatusValidator
,它會通過比對現(xiàn)有的狀態(tài)數(shù)據(jù),驗證輸入值是否為一個有效的狀態(tài)。該驗證器同時支持客戶端以及服務器端驗證。
namespace app\components;
use yii\validators\Validator;
use app\models\Status;
class StatusValidator extends Validator
{
public function init()
{
parent::init();
$this->message = '無效的狀態(tài)輸入。';
}
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);
return <<<JS
if (!$.inArray(value, $statuses)) {
messages.push($message);
}
JS;
}
}
技巧:上述代碼主要是演示了如何支持客戶端驗證。在具體實踐中,你可以使用?in?核心驗證器來達到同樣的目的。比如這樣的驗證規(guī)則:?`````php [
['status', 'in', 'range' => Status::find()->select('id')->asArray()->column()],
]?`````
更多建議: