建立模型
在撰寫表單所需的 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','錯誤的使用者名或密碼。'); } }
上述程式碼指定:username
和 password
為必填項目,
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 的別名,它用於確保所驗證的屬性值不為空。
下面是預定義的驗證器別名的完整列表:
boolean
: CBooleanValidator 的別名, 確保屬性有一個 CBooleanValidator::trueValue 或 CBooleanValidator::falseValue 值。captcha
: CCaptchaValidator 的別名,確保屬性值等於 CAPTCHA 中顯示的驗證碼。compare
: CCompareValidator 的別名,確保屬性等於另一個屬性或常量。email
: CEmailValidator 的別名,確保屬性是一個有效的 Email 位址。default
: CDefaultValueValidator 的別名,指定屬性的預設值。exist
: CExistValidator 的別名,確保屬性值可以在指定表的列中可以找到。file
: CFileValidator 的別名,確保屬性含有一個上傳文件的名字。filter
: CFilterValidator 的別名,通過一個篩選器改變此屬性。in
: CRangeValidator 的別名,確保資料在一個預先指定的值的範圍之內。length
: CStringValidator 的別名,確保資料的長度在一個指定的範圍之內。match
: CRegularExpressionValidator 的別名,確保資料可以匹配一個正則表達式。numerical
: CNumberValidator 的別名,確保資料是一個有效的數字。required
: CRequiredValidator 的別名,確保屬性不為空。type
: CTypeValidator 的別名,確保屬性是指定的資料類型。unique
: CUniqueValidator 的別名,確保資料在資料表的列中是唯一的。url
: CUrlValidator 的別名,確保資料是一個有效的 URL。
下面我們列出了幾個只用這些預定義驗證器的示例:
// 使用者名為必填項目 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'),
如上所示, username
和 password
屬性在 login
場景中是必填項目。而
username
, password
和 email
屬性在 register
場景中是必填項目。
於是,如果我們在 login
場景中執行大量賦值,就只有 username
和 password
會被大量賦值。
因為只有它們出現在 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
場景中,我們只想驗證使用者模型中的 username
和 password
輸入;
而在 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() 方法自定義。 正如在接下來的小節中我們將看到的,在模型中指定標籤會使我們能夠更快的建立出更強大的表單。