建立模型

在撰寫表單所需的 HTML 程式碼之前,我們應該先確定來自使用者輸入的資料的類型,以及這些資料應符合什麼樣的規則。 模型類別可用於記錄這些訊息。 正如模型 章節所定義的, 模型是儲存使用者輸入和驗證這些輸入的中心位置。

取決於使用使用者所輸入資料的方式,我們可以建立兩種類型的模型。 如果使用者輸入被收集、使用然後丟棄,我們應該建立一個 表單模型; 如果使用者的輸入被收集後要儲存到資料庫,我們應使用一個 Active Record 。 兩種類別型的模型共享同樣的基礎類別 CModel ,它定義了表單所需的通用接口。

注意: 我們在這一節的示例中主要使用了表單模型 。然而,同樣的操作也可應用程式於 Active Record 模型。

定義模型類別

下面我們建立了一個 LoginForm 模型類別用於在一個登入頁中收集使用者的輸入。 由於登入訊息只被用於驗證使用者,並不需要儲存,因此我們將 LoginForm 建立為一個 表單模型。

class LoginForm extends CFormModel
{
    public $username;
    public $password;
    public $rememberMe=false;
}

LoginForm 中定義了三個特性: $username, $password$rememberMe。他們用於儲存使用者輸入的使用者名和密碼,還有使用者是否想記住他的登入的選項。 由於 $rememberMe 有一個預設的值 false,相應的選項在初始化顯示在登入表單中時將是未勾選狀態。

訊息: 我們將這些成員變數稱為 屬性(attributes) 而不是 特性(properties),以區別於普通的特性(properties)。 屬性(attribute)是一個主要用於存儲來自使用者輸入或資料庫資料的特性(propertiy)。

宣告驗證規則

一旦使用者提交了他的輸入給模型,我們就需要在使用前確保使用者的輸入是有效的。 這是通過將使用者的輸入和一系列規則執行驗證實現的。我們在 rules() 方法中指定這些驗證規則, 此方法應返回一個規則配置陣列。

class LoginForm extends CFormModel
{
    public $username;
    public $password;
    public $rememberMe=false;
 
    private $_identity;
 
    public function rules()
    {
        return array(
            array('username, password', 'required'),
            array('rememberMe', 'boolean'),
            array('password', 'authenticate'),
        );
    }
 
    public function authenticate($attribute,$params)
    {
        $this->_identity=new UserIdentity($this->username,$this->password);
        if(!$this->_identity->authenticate())
            $this->addError('password','錯誤的使用者名或密碼。');
    }
}

上述程式碼指定:usernamepassword 為必填項目, password 應被驗證(authenticated),rememberMe 應該是一個布林值。

rules() 返回的每個規則必須是以下格式:

array('AttributeList', 'Validator', 'on'=>'ScenarioList', ...附加選項)

其中 AttributeList(屬性列表) 是需要通過此規則驗證的屬性列表字串,每個屬性名字由逗號分隔; Validator(驗證器) 指定要執行驗證的種類別;on 參數是可選的,它指定此規則應被應用程式到的場景列表; 附加選項是一個名值對陣列,用於初始化相應驗證器的特性值。

有三種方式可在驗證規則中指定 Validator 。第一, Validator 可以是模型類別中一個方法的名字,就像上面示例中的 authenticate 。驗證方法必須是下面的架構:

/**
 * @param string 所要驗證的屬性的名字
 * @param array 驗證規則中指定的選項
 */
public function ValidatorName($attribute,$params) { ... }

第二,Validator 可以是一個驗證器類別的名字,當此規則被應用程式時, 一個驗證器類別的實體將被建立以執行實際驗證。規則中的附加選項用於初始化實體的特性值。 驗證器類別必須繼承自 CValidator

第三,Validator 可以是一個預定義的驗證器類別的別名。在上面的例子中, required 名字是 CRequiredValidator 的別名,它用於確保所驗證的屬性值不為空。 下面是預定義的驗證器別名的完整列表:

下面我們列出了幾個只用這些預定義驗證器的示例:

// 使用者名為必填項目
array('username', 'required'),
// 使用者名必須在 3 到 12 個字符之間
array('username', 'length', 'min'=>3, 'max'=>12),
// 在註冊場景中,密碼 password 必須和 password2 一致。
array('password', 'compare', 'compareAttribute'=>'password2', 'on'=>'register'),
// 在登入場景中,密碼必須接受驗證。
array('password', 'authenticate', 'on'=>'login'),

安全的屬性賦值

在一個類別的實體被建立後,我們通常需要用最終使用者提交的資料填入它的屬性。 這可以通過如下大量賦值(massive assignment)方式輕鬆實現:

$model=new LoginForm;
if(isset($_POST['LoginForm']))
    $model->attributes=$_POST['LoginForm'];

最後的表達式被稱作 大量賦值(massive assignment) ,它將 $_POST['LoginForm'] 中的每一項複製到相應的模型屬性中。這相當於如下賦值方法:

foreach($_POST['LoginForm'] as $name=>$value)
{
    if($name is a safe attribute)
        $model->$name=$value;
}

檢測屬性的安全非常重要,例如,如果我們以為一個資料表的主鍵是安全的而暴露了它,那麼攻擊者可能就獲得了一個修改記錄的主鍵的機會, 從而篡改未授權給他的內容。

檢測屬性安全的策略在版本 1.0 和 1.1 中是不同的,下面我們將分別講解:

宣告安全屬性

屬性如果出現在相應場景的一個驗證規則中,即被認為是安全的。例如:

array('username, password', 'required', 'on'=>'login, register'),
array('email', 'required', 'on'=>'register'),

如上所示, usernamepassword 屬性在 login 場景中是必填項目。而 username, passwordemail 屬性在 register 場景中是必填項目。 於是,如果我們在 login 場景中執行大量賦值,就只有 usernamepassword 會被大量賦值。 因為只有它們出現在 login 的驗證規則中。 另一方面,如果場景是 register ,這三個屬性就都可以被大量賦值。

// 在登入場景中
$model=new User('login');
if(isset($_POST['User']))
    $model->attributes=$_POST['User'];
 
// 在註冊場景中
$model=new User('register');
if(isset($_POST['User']))
    $model->attributes=$_POST['User'];

那麼為什麼我們使用這樣一種策略來檢測屬性是否安全呢? 背後的基本原理就是:如果一個屬性已經有了一個或多個可檢測有效性的驗證規則,那我們還擔心什麼呢?

請記住,驗證規則是用於檢查使用者輸入的資料,而不是檢查我們在程式碼中產生的資料(例如時間戳,自動產生的主鍵)。 因此,不要 為那些不接受使用者輸入的屬性增加驗證規則。

有時候,我們想聲明一個屬性是安全的,即使我們沒有為它指定任何規則。 例如,一篇文章的內容可以接受使用者的任何輸入。我們可以使用特殊的 safe 規則實現此目的:

array('content', 'safe')

為了完成起見,還有一個用於聲明一個特性為不安全的 unsafe 規則:

array('permission', 'unsafe')

unsafe 規則並不常用,它是我們之前定義的安全屬性的一個例外。

觸發驗證

一旦模型被使用者提交的資料填入,我們就可以調用 CModel::validate() 觸發資料驗證進程。此方法返回一個指示驗證是否成功的值。 對 CActiveRecord 模型來說,驗證也可以在我們調用其 CActiveRecord::save() 方法時自動觸發。

我們可以使用 scenario 設置場景特性,這樣,相應場景的驗證規則就會被應用。

驗證是基於場景執行的。 scenario 特性指定了模型當前用於的場景和當前使用的驗證規則集。 例如,在 login 場景中,我們只想驗證使用者模型中的 usernamepassword 輸入; 而在 register 場景中,我們需要驗證更多的輸入,例如 email, address, 等。 下面的例子展示了如何在 register 場景中執行驗證:

// 在註冊場景中建立一個  User 模型。等於:
// $model=new User;
// $model->scenario='register';
$model=new User('register');
 
// 將輸入的值填入到模型
$model->attributes=$_POST['User'];
 
// 執行驗證
if($model->validate())   // if the inputs are valid
    ...
else
    ...

規則關聯的場景可以通過規則中的 on 選項指定。如果 on 選項未設置,則此規則會應用程式於所有場景。例如:

public function rules()
{
    return array(
        array('username, password', 'required'),
        array('password_repeat', 'required', 'on'=>'register'),
        array('password', 'compare', 'on'=>'register'),
    );
}

第一個規則將應用程式於所有場景,而第二個將只會應用程式於 register 場景。

擷取驗證錯誤

驗證完成後,任何可能產生的錯誤將被存儲在模型對像中。 我們可以通過調用 CModel::getErrors()CModel::getError() 擷取這些錯誤訊息。 這兩個方法的不同點在於第一個方法將返回 所有 模型屬性的錯誤訊息,而第二個將只返回 第一個 錯誤訊息。

屬性標籤

當設計表單時,我們通常需要為每個表單域顯示一個標籤。 標籤告訴使用者他應該在此表單域中填寫什麼樣的訊息。雖然我們可以在視圖中寫死一個標籤, 但如果我們在相應的模型中指定(標籤),則會更加靈活方便。

預設情況下 CModel 將簡單的返回屬性的名字作為其標籤。這可以通過覆蓋 attributeLabels() 方法自定義。 正如在接下來的小節中我們將看到的,在模型中指定標籤會使我們能夠更快的建立出更強大的表單。

$Id$