輸入驗(yàn)證(Validating Input)

2018-02-24 15:40 更新

輸入驗(yàn)證

一般說來,程序猿永遠(yuǎn)不應(yīng)該信任從最終用戶直接接收到的數(shù)據(jù),并且使用它們之前應(yīng)始終先驗(yàn)證其可靠性。

要給?model?填充其所需的用戶輸入數(shù)據(jù),你可以調(diào)用 yii\base\Model::validate() 方法驗(yàn)證它們。該方法會返回一個(gè)布爾值,指明是否通過驗(yàn)證。若沒有通過,你能通過 yii\base\Model::errors 屬性獲取相應(yīng)的報(bào)錯(cuò)信息。比如,

$model = new \app\models\ContactForm;

// 用用戶輸入來填充模型的特性
$model->attributes = \Yii::$app->request->post('ContactForm');

if ($model->validate()) {
    // 若所有輸入都是有效的
} else {
    // 有效性驗(yàn)證失?。?errors 屬性就是存儲錯(cuò)誤信息的數(shù)組
    $errors = $model->errors;
}

validate()?方法,在幕后為執(zhí)行驗(yàn)證操作,進(jìn)行了以下步驟:

  1. 通過從 yii\base\Model::scenarios() 方法返回基于當(dāng)前 yii\base\Model::scenario 的特性屬性列表,算出哪些特性應(yīng)該進(jìn)行有效性驗(yàn)證。這些屬性被稱作?active attributes(激活特性)。
  2. 通過從 yii\base\Model::rules() 方法返回基于當(dāng)前 yii\base\Model::scenario 的驗(yàn)證規(guī)則列表,這些規(guī)則被稱作?active rules(激活規(guī)則)。
  3. 用每個(gè)激活規(guī)則去驗(yàn)證每個(gè)與之關(guān)聯(lián)的激活特性。若失敗,則記錄下對應(yīng)模型特性的錯(cuò)誤信息。

聲明規(guī)則(Rules)

要讓?validate()?方法起作用,你需要聲明與需驗(yàn)證模型特性相關(guān)的驗(yàn)證規(guī)則。為此,需要重寫 yii\base\Model::rules() 方法。下面的例子展示了如何聲明用于驗(yàn)證?ContactForm?模型的相關(guān)驗(yàn)證規(guī)則:

public function rules()
{
    return [
        // name,email,subject 和 body 特性是 `require`(必填)的
        [['name', 'email', 'subject', 'body'], 'required'],

        // email 特性必須是一個(gè)有效的 email 地址
        ['email', 'email'],
    ];
}

yii\base\Model::rules() 方法應(yīng)返回一個(gè)由規(guī)則所組成的數(shù)組,每一個(gè)規(guī)則都呈現(xiàn)為以下這類格式的小數(shù)組:

[
    // 必須項(xiàng),用于指定那些模型特性需要通過此規(guī)則的驗(yàn)證。
    // 對于只有一個(gè)特性的情況,可以直接寫特性名,而不必用數(shù)組包裹。
    ['attribute1', 'attribute2', ...],

    // 必填項(xiàng),用于指定規(guī)則的類型。
    // 它可以是類名,驗(yàn)證器昵稱,或者是驗(yàn)證方法的名稱。
    'validator',

    // 可選項(xiàng),用于指定在場景(scenario)中,需要啟用該規(guī)則
    // 若不提供,則代表該規(guī)則適用于所有場景
    // 若你需要提供除了某些特定場景以外的所有其他場景,你也可以配置 "except" 選項(xiàng)
    'on' => ['scenario1', 'scenario2', ...],

    // 可選項(xiàng),用于指定對該驗(yàn)證器對象的其他配置選項(xiàng)
    'property1' => 'value1', 'property2' => 'value2', ...
]

對于每個(gè)規(guī)則,你至少需要指定該規(guī)則適用于哪些特性,以及本規(guī)則的類型是什么。你可以指定以下的規(guī)則類型之一:

  • 核心驗(yàn)證器的昵稱,比如?requiredin、date,等等。請參考核心驗(yàn)證器章節(jié)查看完整的核心驗(yàn)證器列表。
  • 模型類中的某個(gè)驗(yàn)證方法的名稱,或者一個(gè)匿名方法。請參考行內(nèi)驗(yàn)證器小節(jié)了解更多。
  • 驗(yàn)證器類的名稱。請參考獨(dú)立驗(yàn)證器小節(jié)了解更多。

一個(gè)規(guī)則可用于驗(yàn)證一個(gè)或多個(gè)模型特性,且一個(gè)特性可以被一個(gè)或多個(gè)規(guī)則所驗(yàn)證。一個(gè)規(guī)則可以施用于特定場景(scenario),只要指定?on?選項(xiàng)。如果你不指定?on?選項(xiàng),那么該規(guī)則會適配于所有場景。

當(dāng)調(diào)用?validate()?方法時(shí),它將運(yùn)行以下幾個(gè)具體的驗(yàn)證步驟:

  1. 檢查從聲明自 yii\base\Model::scenarios() 方法的場景中所挑選出的當(dāng)前yii\base\Model::scenario的信息,從而確定出那些特性需要被驗(yàn)證。這些特性被稱為激活特性。
  2. 檢查從聲明自 yii\base\Model::rules() 方法的眾多規(guī)則中所挑選出的適用于當(dāng)前yii\base\Model::scenario的規(guī)則,從而確定出需要驗(yàn)證哪些規(guī)則。這些規(guī)則被稱為激活規(guī)則。
  3. 用每個(gè)激活規(guī)則去驗(yàn)證每個(gè)與之關(guān)聯(lián)的激活特性。

基于以上驗(yàn)證步驟,有且僅有聲明在?scenarios()?方法里的激活特性,且它還必須與一或多個(gè)聲明自?rules()?里的激活規(guī)則相關(guān)聯(lián)才會被驗(yàn)證。

自定義錯(cuò)誤信息

大多數(shù)的驗(yàn)證器都有默認(rèn)的錯(cuò)誤信息,當(dāng)模型的某個(gè)特性驗(yàn)證失敗的時(shí)候,該錯(cuò)誤信息會被返回給模型。比如,用 yii\validators\RequiredValidator 驗(yàn)證器的規(guī)則檢驗(yàn)?username?特性失敗的話,會返還給模型 "Username cannot be blank." 信息。

你可以通過在聲明規(guī)則的時(shí)候同時(shí)指定?message?屬性,來定制某個(gè)規(guī)則的錯(cuò)誤信息,比如這樣:

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

一些驗(yàn)證器還支持用于針對不同原因的驗(yàn)證失敗返回更加準(zhǔn)確的額外錯(cuò)誤信息。比如,yii\validators\NumberValidator 驗(yàn)證器就支持 yii\validators\NumberValidator::tooBig 和 yii\validators\NumberValidator::tooSmall 兩種錯(cuò)誤消息用于分別返回輸入值是太大還是太小。 你也可以像配置驗(yàn)證器的其他屬性一樣配置它們倆各自的錯(cuò)誤信息。

驗(yàn)證事件

當(dāng)調(diào)用 yii\base\Model::validate() 方法的過程里,它同時(shí)會調(diào)用兩個(gè)特殊的方法,把它們重寫掉可以實(shí)現(xiàn)自定義驗(yàn)證過程的目的:

  • yii\base\Model::beforeValidate():在默認(rèn)的實(shí)現(xiàn)中會觸發(fā) yii\base\Model::EVENT_BEFORE_VALIDATE 事件。你可以重寫該方法或者響應(yīng)此事件,來在驗(yàn)證開始之前,先進(jìn)行一些預(yù)處理的工作。(比如,標(biāo)準(zhǔn)化數(shù)據(jù)輸入)該方法應(yīng)該返回一個(gè)布爾值,用于標(biāo)明驗(yàn)證是否通過。
  • yii\base\Model::afterValidate():在默認(rèn)的實(shí)現(xiàn)中會觸發(fā) yii\base\Model::EVENT_AFTER_VALIDATE 事件。你可以重寫該方法或者響應(yīng)此事件,來在驗(yàn)證結(jié)束之后,再進(jìn)行一些收尾的工作。

條件式驗(yàn)證

若要只在某些條件滿足時(shí),才驗(yàn)證相關(guān)特性,比如:是否驗(yàn)證某特性取決于另一特性的值,你可以通過 yii\validators\Validator::when 屬性來定義相關(guān)條件。舉例而言,

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

yii\validators\Validator::when 屬性會讀入一個(gè)如下所示結(jié)構(gòu)的 PHP callable 函數(shù)對象:

/**
 * @param Model $model 要驗(yàn)證的模型對象
 * @param string $attribute 待測特性名
 * @return boolean 返回是否啟用該規(guī)則
 */
function ($model, $attribute)

若你需要支持客戶端的條件驗(yàn)證,你應(yīng)該配置 yii\validators\Validator::whenClient 屬性,它會讀入一條包含有 JavaScript 函數(shù)的字符串。這個(gè)函數(shù)將被用于確定該客戶端驗(yàn)證規(guī)則是否被啟用。比如,

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

數(shù)據(jù)預(yù)處理

用戶輸入經(jīng)常需要進(jìn)行數(shù)據(jù)過濾,或者叫預(yù)處理。比如你可能會需要先去掉?username?輸入的收尾空格。你可以通過使用驗(yàn)證規(guī)則來實(shí)現(xiàn)此目的。

下面的例子展示了如何去掉輸入信息的首尾空格,并將空輸入返回為 null。具體方法為通過調(diào)用?trim?和?default?核心驗(yàn)證器:

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

也還可以用更加通用的?filter(濾鏡)?核心驗(yàn)證器來執(zhí)行更加復(fù)雜的數(shù)據(jù)過濾。

如你所見,這些驗(yàn)證規(guī)則并不真的對輸入數(shù)據(jù)進(jìn)行任何驗(yàn)證。而是,對輸入數(shù)據(jù)進(jìn)行一些處理,然后把它們存回當(dāng)前被驗(yàn)證的模型特性。

處理空輸入

當(dāng)輸入數(shù)據(jù)是通過 HTML 表單,你經(jīng)常會需要給空的輸入項(xiàng)賦默認(rèn)值。你可以通過調(diào)整?default?驗(yàn)證器來實(shí)現(xiàn)這一點(diǎn)。舉例來說,

[
    // 若 "username" 和 "email" 為空,則設(shè)為 null
    [['username', 'email'], 'default'],

    // 若 "level" 為空,則設(shè)其為 1
    ['level', 'default', 'value' => 1],
]

默認(rèn)情況下,當(dāng)輸入項(xiàng)為空字符串,空數(shù)組,或 null 時(shí),會被視為“空值”。你也可以通過配置 yii\validators\Validator::isEmpty 屬性來自定義空值的判定規(guī)則。比如,

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

注意:對于絕大多數(shù)驗(yàn)證器而言,若其 yii\base\Validator::skipOnEmpty 屬性為默認(rèn)值 true,則它們不會對空值進(jìn)行任何處理。也就是當(dāng)他們的關(guān)聯(lián)特性接收到空值時(shí),相關(guān)驗(yàn)證會被直接略過。在?核心驗(yàn)證器?之中,只有?captcha(驗(yàn)證碼),default(默認(rèn)值),filter(濾鏡),required(必填),以及?trim(去首尾空格),這幾個(gè)驗(yàn)證器會處理空輸入。

臨時(shí)驗(yàn)證

有時(shí),你需要對某些沒有綁定任何模型類的值進(jìn)行?臨時(shí)驗(yàn)證。

若你只需要進(jìn)行一種類型的驗(yàn)證 (e.g. 驗(yàn)證郵箱地址),你可以調(diào)用所需驗(yàn)證器的 yii\validators\Validator::validate() 方法。像這樣:

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

if ($validator->validate($email, $error)) {
    echo '有效的 Email 地址。';
} else {
    echo $error;
}

注意:不是所有的驗(yàn)證器都支持這種形式的驗(yàn)證。比如?unique(唯一性)核心驗(yàn)證器就就是一個(gè)例子,它的設(shè)計(jì)初衷就是只作用于模型類內(nèi)部的。

若你需要針對一系列值執(zhí)行多項(xiàng)驗(yàn)證,你可以使用 yii\base\DynamicModel 。它支持即時(shí)添加特性和驗(yàn)證規(guī)則的定義。它的使用規(guī)則是這樣的:

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

    if ($model->hasErrors()) {
        // 驗(yàn)證失敗
    } else {
        // 驗(yàn)證成功
    }
}

yii\base\DynamicModel::validateData() 方法會創(chuàng)建一個(gè)?DynamicModel?的實(shí)例對象,并通過給定數(shù)據(jù)定義模型特性(以?name?和email?為例),之后用給定規(guī)則調(diào)用 yii\base\Model::validate() 方法。

除此之外呢,你也可以用如下的更加“傳統(tǒng)”的語法來執(zhí)行臨時(shí)數(shù)據(jù)驗(yàn)證:

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()) {
        // 驗(yàn)證失敗
    } else {
        // 驗(yàn)證成功
    }
}

驗(yàn)證之后你可以通過調(diào)用 yii\base\DynamicModel::hasErrors() 方法來檢查驗(yàn)證通過與否,并通過 yii\base\DynamicModel::errors 屬性獲得驗(yàn)證的錯(cuò)誤信息,過程與普通模型類一致。你也可以訪問模型對象內(nèi)定義的動態(tài)特性,就像:?$model->name?和?$model->email。

創(chuàng)建驗(yàn)證器(Validators)

除了使用 Yii 的發(fā)布版里所包含的核心驗(yàn)證器之外,你也可以創(chuàng)建你自己的驗(yàn)證器。自定義的驗(yàn)證器可以是行內(nèi)驗(yàn)證器,也可以是獨(dú)立驗(yàn)證器。

行內(nèi)驗(yàn)證器(Inline Validators)

行內(nèi)驗(yàn)證器是一種以模型方法或匿名函數(shù)的形式定義的驗(yàn)證器。這些方法/函數(shù)的結(jié)構(gòu)如下:

/**
 * @param string $attribute 當(dāng)前被驗(yàn)證的特性
 * @param array $params 以名-值對形式提供的額外參數(shù)
 */
function ($attribute, $params)

若某特性的驗(yàn)證失敗了,該方法/函數(shù)應(yīng)該調(diào)用 yii\base\Model::addError() 保存錯(cuò)誤信息到模型內(nèi)。這樣這些錯(cuò)誤就能在之后的操作中,被讀取并展現(xiàn)給終端用戶。

下面是一些例子:

use yii\base\Model;

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

    public function rules()
    {
        return [
            // 以模型方法 validateCountry() 形式定義的行內(nèi)驗(yàn)證器
            ['country', 'validateCountry'],

            // 以匿名函數(shù)形式定義的行內(nèi)驗(yàn)證器
            ['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)驗(yàn)證器不會在關(guān)聯(lián)特性的輸入值為空或該特性已經(jīng)在其他驗(yàn)證中失敗的情況下起效。若你想要確保該驗(yàn)證器始終啟用的話,你可以在定義規(guī)則時(shí),酌情將 yii\validators\Validator::skipOnEmpty 以及 yii\validators\Validator::skipOnError 屬性設(shè)為 false,比如,?`````php [

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

]?`````

獨(dú)立驗(yàn)證器(Standalone Validators)

獨(dú)立驗(yàn)證器是繼承自 yii\validators\Validator 或其子類的類。你可以通過重寫 yii\validators\Validator::validateAttribute() 來實(shí)現(xiàn)它的驗(yàn)證規(guī)則。若特性驗(yàn)證失敗,可以調(diào)用 yii\base\Model::addError() 以保存錯(cuò)誤信息到模型內(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".');
        }
    }
}

若你想要驗(yàn)證器支持不使用 model 的數(shù)據(jù)驗(yàn)證,你還應(yīng)該重寫 yii\validators\Validator::validate() 方法。你也可以通過重寫 yii\validators\Validator::validateValue() 方法替代?validateAttribute()?和?validate(),因?yàn)槟J(rèn)狀態(tài)下,后兩者的實(shí)現(xiàn)使用過調(diào)用?validateValue()實(shí)現(xiàn)的。

客戶端驗(yàn)證器(Client-Side Validation)

當(dāng)終端用戶通過 HTML 表單提供相關(guān)輸入信息時(shí),我們可能會需要用到基于 JavaScript 的客戶端驗(yàn)證。因?yàn)?,它可以讓用戶更快速的得到錯(cuò)誤信息,也因此可以提供更好的用戶體驗(yàn)。你可以使用或自己實(shí)現(xiàn)除服務(wù)器端驗(yàn)證之外,還能額外客戶端驗(yàn)證功能的驗(yàn)證器。

補(bǔ)充:盡管客戶端驗(yàn)證為加分項(xiàng),但它不是必須項(xiàng)。它存在的主要意義在于給用戶提供更好的客戶體驗(yàn)。正如“永遠(yuǎn)不要相信來自終端用戶的輸入信息”,也同樣永遠(yuǎn)不要相信客戶端驗(yàn)證?;谶@個(gè)理由,你應(yīng)該始終如前文所描述的那樣,通過調(diào)用 yii\base\Model::validate() 方法執(zhí)行服務(wù)器端驗(yàn)證。

使用客戶端驗(yàn)證

許多核心驗(yàn)證器都支持開箱即用的客戶端驗(yàn)證。你只需要用 yii\widgets\ActiveForm 的方式構(gòu)建 HTML 表單即可。比如,下面的LoginForm(登錄表單)聲明了兩個(gè)規(guī)則:其一為?required?核心驗(yàn)證器,它同時(shí)支持客戶端與服務(wù)器端的驗(yàn)證;另一個(gè)則采用validatePassword?行內(nèi)驗(yàn)證器,它只支持服務(wù)器端。

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 都是必填項(xiàng)
            [['username', 'password'], 'required'],

            // 用 validatePassword() 驗(yàn)證 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 表單包含兩個(gè)輸入框?username?以及?password。如果你在沒有輸入任何東西之前提交表單,就會在沒有任何與服務(wù)器端的通訊的情況下,立刻收到一個(gè)要求你填寫空白項(xiàng)的錯(cuò)誤信息。

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

幕后的運(yùn)作過程是這樣的:yii\widgets\ActiveForm 會讀取聲明在模型類中的驗(yàn)證規(guī)則,并生成那些支持支持客戶端驗(yàn)證的驗(yàn)證器所需的 JavaScript 代碼。當(dāng)用戶修改輸入框的值,或者提交表單時(shí),就會觸發(fā)相應(yīng)的客戶端驗(yàn)證 JS 代碼。

若你需要完全關(guān)閉客戶端驗(yàn)證,你只需配置 yii\widgets\ActiveForm::enableClientValidation 屬性為 false。你同樣可以關(guān)閉各個(gè)輸入框各自的客戶端驗(yàn)證,只要把它們的 yii\widgets\ActiveField::enableClientValidation 屬性設(shè)為 false。

自己實(shí)現(xiàn)客戶端驗(yàn)證

要穿件一個(gè)支持客戶端驗(yàn)證的驗(yàn)證器,你需要實(shí)現(xiàn) yii\validators\Validator::clientValidateAttribute() 方法,用于返回一段用于運(yùn)行客戶端驗(yàn)證的 JavaScript 代碼。在這段 JavaScript 代碼中,你可以使用以下預(yù)定義的變量:

  • attribute:正在被驗(yàn)證的模型特性的名稱。
  • value:進(jìn)行驗(yàn)證的值。
  • messages:一個(gè)用于暫存模型特性的報(bào)錯(cuò)信息的數(shù)組。

在下面的例子里,我們會創(chuàng)建一個(gè)?StatusValidator,它會通過比對現(xiàn)有的狀態(tài)數(shù)據(jù),驗(yàn)證輸入值是否為一個(gè)有效的狀態(tài)。該驗(yàn)證器同時(shí)支持客戶端以及服務(wù)器端驗(yàn)證。

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;
    }
}

技巧:上述代碼主要是演示了如何支持客戶端驗(yàn)證。在具體實(shí)踐中,你可以使用?in?核心驗(yàn)證器來達(dá)到同樣的目的。比如這樣的驗(yàn)證規(guī)則:?`````php [

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

]?`````

以上內(nèi)容是否對您有幫助:
在線筆記
App下載
App下載

掃描二維碼

下載編程獅App

公眾號
微信公眾號

編程獅公眾號