# 模型
模型是?[MVC](http://en.wikipedia.org/wiki/Model%E2%80%93view%E2%80%93controller)?模式中的一部分, 是代表業務數據、規則和邏輯的對象。
可通過繼承 yii\base\Model 或它的子類定義模型類,基類yii\base\Model支持許多實用的特性:
* [屬性](http://www.yiichina.com/doc/guide/2.0/structure-models#attributes): 代表可像普通類屬性或數組一樣被訪問的業務數據;
* [屬性標簽](http://www.yiichina.com/doc/guide/2.0/structure-models#attribute-labels): 指定屬性顯示出來的標簽;
* [塊賦值](http://www.yiichina.com/doc/guide/2.0/structure-models#massive-assignment): 支持一步給許多屬性賦值;
* [驗證規則](http://www.yiichina.com/doc/guide/2.0/structure-models#validation-rules): 確保輸入數據符合所申明的驗證規則;
* [數據導出](http://www.yiichina.com/doc/guide/2.0/structure-models#data-exporting): 允許模型數據導出為自定義格式的數組。
`Model`?類也是更多高級模型如[Active Record 活動記錄](http://www.yiichina.com/doc/guide/2.0/db-active-record)的基類, 更多關于這些高級模型的詳情請參考相關手冊。
> 補充:模型并不強制一定要繼承yii\base\Model,但是由于很多組件支持yii\base\Model,最好使用它做為模型基類。
## 屬性
模型通過?*屬性*?來代表業務數據,每個屬性像是模型的公有可訪問屬性, yii\base\Model::attributes() 指定模型所擁有的屬性。
可像訪問一個對象屬性一樣訪問模型的屬性:
~~~
$model = new \app\models\ContactForm;
// "name" 是ContactForm模型的屬性
$model->name = 'example';
echo $model->name;
~~~
也可像訪問數組單元項一樣訪問屬性,這要感謝yii\base\Model支持?[ArrayAccess 數組訪問](http://php.net/manual/en/class.arrayaccess.php)?和?[ArrayIterator 數組迭代器](http://php.net/manual/en/class.arrayiterator.php):
~~~
$model = new \app\models\ContactForm;
// 像訪問數組單元項一樣訪問屬性
$model['name'] = 'example';
echo $model['name'];
// 迭代器遍歷模型
foreach ($model as $name => $value) {
echo "$name: $value\n";
}
~~~
### 定義屬性
默認情況下你的模型類直接從yii\base\Model繼承,所有?*non-static public非靜態公有*?成員變量都是屬性。 例如,下述`ContactForm`模型類有四個屬性`name`,?`email`,?`subject`?and?`body`,?`ContactForm`?模型用來代表從HTML表單獲取的輸入數據。
~~~
namespace app\models;
use yii\base\Model;
class ContactForm extends Model
{
public $name;
public $email;
public $subject;
public $body;
}
~~~
另一種方式是可覆蓋 yii\base\Model::attributes() 來定義屬性,該方法返回模型的屬性名。 例如 yii\db\ActiveRecord 返回對應數據表列名作為它的屬性名, 注意可能需要覆蓋魔術方法如`__get()`,?`__set()`使屬性像普通對象屬性被訪問。
### 屬性標簽
當屬性顯示或獲取輸入時,經常要顯示屬性相關標簽,例如假定一個屬性名為`firstName`, 在某些地方如表單輸入或錯誤信息處,你可能想顯示對終端用戶來說更友好的?`First Name`?標簽。
可以調用 yii\base\Model::getAttributeLabel() 獲取屬性的標簽,例如:
~~~
$model = new \app\models\ContactForm;
// 顯示為 "Name"
echo $model->getAttributeLabel('name');
~~~
默認情況下,屬性標簽通過yii\base\Model::generateAttributeLabel()方法自動從屬性名生成. 它會自動將駝峰式大小寫變量名轉換為多個首字母大寫的單詞,例如?`username`?轉換為?`Username`,?`firstName`?轉換為?`First Name`。
如果你不想用自動生成的標簽,可以覆蓋 yii\base\Model::attributeLabels() 方法明確指定屬性標簽,例如:
~~~
namespace app\models;
use yii\base\Model;
class ContactForm extends Model
{
public $name;
public $email;
public $subject;
public $body;
public function attributeLabels()
{
return [
'name' => 'Your name',
'email' => 'Your email address',
'subject' => 'Subject',
'body' => 'Content',
];
}
}
~~~
應用支持多語言的情況下,可翻譯屬性標簽, 可在 yii\base\Model::attributeLabels() 方法中定義,如下所示:
~~~
public function attributeLabels()
{
return [
'name' => \Yii::t('app', 'Your name'),
'email' => \Yii::t('app', 'Your email address'),
'subject' => \Yii::t('app', 'Subject'),
'body' => \Yii::t('app', 'Content'),
];
}
~~~
甚至可以根據條件定義標簽,例如通過使用模型的?[scenario場景](http://www.yiichina.com/doc/guide/2.0/structure-models#scenarios), 可對相同的屬性返回不同的標簽。
> 補充:屬性標簽是?[視圖](http://www.yiichina.com/doc/guide/2.0/structure-views)一部分,但是在模型中申明標簽通常非常方便,并可行程非常簡潔可重用代碼。
## 場景
模型可能在多個?*場景*?下使用,例如?`User`?模塊可能會在收集用戶登錄輸入,也可能會在用戶注冊時使用。 在不同的場景下,模型可能會使用不同的業務規則和邏輯,例如?`email`?屬性在注冊時強制要求有,但在登陸時不需要。
模型使用 yii\base\Model::scenario 屬性保持使用場景的跟蹤, 默認情況下,模型支持一個名為?`default`?的場景,如下展示兩種設置場景的方法:
~~~
// 場景作為屬性來設置
$model = new User;
$model->scenario = 'login';
// 場景通過構造初始化配置來設置
$model = new User(['scenario' => 'login']);
~~~
默認情況下,模型支持的場景由模型中申明的?[驗證規則](http://www.yiichina.com/doc/guide/2.0/structure-models#validation-rules)?來決定, 但你可以通過覆蓋yii\base\Model::scenarios()方法來自定義行為,如下所示:
~~~
namespace app\models;
use yii\db\ActiveRecord;
class User extends ActiveRecord
{
public function scenarios()
{
return [
'login' => ['username', 'password'],
'register' => ['username', 'email', 'password'],
];
}
}
~~~
> 補充:在上述和下述的例子中,模型類都是繼承yii\db\ActiveRecord, 因為多場景的使用通常發生在[Active Record](http://www.yiichina.com/doc/guide/2.0/db-active-record)?類中.
`scenarios()`?方法返回一個數組,數組的鍵為場景名,值為對應的?*active attributes活動屬性*。 活動屬性可被?[塊賦值](http://www.yiichina.com/doc/guide/2.0/structure-models#massive-assignment)?并遵循[驗證規則](http://www.yiichina.com/doc/guide/2.0/structure-models#validation-rules)在上述例子中,`username`?和?`password`?在`login`場景中啟用,在?`register`?場景中, 除了?`username`?and?`password`?外?`email`也被啟用。
`scenarios()`?方法默認實現會返回所有yii\base\Model::rules()方法申明的驗證規則中的場景, 當覆蓋`scenarios()`時,如果你想在默認場景外使用新場景,可以編寫類似如下代碼:
~~~
namespace app\models;
use yii\db\ActiveRecord;
class User extends ActiveRecord
{
public function scenarios()
{
$scenarios = parent::scenarios();
$scenarios['login'] = ['username', 'password'];
$scenarios['register'] = ['username', 'email', 'password'];
return $scenarios;
}
}
~~~
場景特性主要在[驗證](http://www.yiichina.com/doc/guide/2.0/structure-models#validation-rules)?和?[屬性塊賦值](http://www.yiichina.com/doc/guide/2.0/structure-models#massive-assignment)?中使用。 你也可以用于其他目的,例如可基于不同的場景定義不同的?[屬性標簽](http://www.yiichina.com/doc/guide/2.0/structure-models#attribute-labels)。
## 驗證規則
當模型接收到終端用戶輸入的數據,數據應當滿足某種規則(稱為?*驗證規則*, 也稱為?*業務規則*)。 例如假定`ContactForm`模型,你可能想確保所有屬性不為空且?`email`?屬性包含一個有效的郵箱地址, 如果某個屬性的值不滿足對應的業務規則,相應的錯誤信息應顯示,以幫助用戶修正錯誤。
可調用 yii\base\Model::validate() 來驗證接收到的數據, 該方法使用yii\base\Model::rules()申明的驗證規則來驗證每個相關屬性, 如果沒有找到錯誤,會返回 true,否則它會將錯誤保存在 yii\base\Model::errors 屬性中并返回false,例如:
~~~
$model = new \app\models\ContactForm;
// 用戶輸入數據賦值到模型屬性
$model->attributes = \Yii::$app->request->post('ContactForm');
if ($model->validate()) {
// 所有輸入數據都有效 all inputs are valid
} else {
// 驗證失敗:$errors 是一個包含錯誤信息的數組
$errors = $model->errors;
}
~~~
通過覆蓋 yii\base\Model::rules() 方法指定模型屬性應該滿足的規則來申明模型相關驗證規則。 下述例子顯示`ContactForm`模型申明的驗證規則:
~~~
public function rules()
{
return [
// name, email, subject 和 body 屬性必須有值
[['name', 'email', 'subject', 'body'], 'required'],
// email 屬性必須是一個有效的電子郵箱地址
['email', 'email'],
];
}
~~~
一條規則可用來驗證一個或多個屬性,一個屬性可對應一條或多條規則。 更多關于如何申明驗證規則的詳情請參考?[驗證輸入](http://www.yiichina.com/doc/guide/2.0/input-validation)?一節.
有時你想一條規則只在某個?[場景](http://www.yiichina.com/doc/guide/2.0/structure-models#scenarios)?下應用,為此你可以指定規則的?`on`?屬性,如下所示:
~~~
public function rules()
{
return [
// 在"register" 場景下 username, email 和 password 必須有值
[['username', 'email', 'password'], 'required', 'on' => 'register'],
// 在 "login" 場景下 username 和 password 必須有值
[['username', 'password'], 'required', 'on' => 'login'],
];
}
~~~
如果沒有指定?`on`?屬性,規則會在所有場景下應用, 在當前yii\base\Model::scenario 下應用的規則稱之為?*active rule活動規則*。
一個屬性只會屬于`scenarios()`中定義的活動屬性且在`rules()`申明對應一條或多條活動規則的情況下被驗證。
## 塊賦值
塊賦值只用一行代碼將用戶所有輸入填充到一個模型,非常方便, 它直接將輸入數據對應填充到 yii\base\Model::attributes 屬性。 以下兩段代碼效果是相同的,都是將終端用戶輸入的表單數據賦值到?`ContactForm`?模型的屬性, 明顯地前一段塊賦值的代碼比后一段代碼簡潔且不易出錯。
~~~
$model = new \app\models\ContactForm;
$model->attributes = \Yii::$app->request->post('ContactForm');
~~~
~~~
$model = new \app\models\ContactForm;
$data = \Yii::$app->request->post('ContactForm', []);
$model->name = isset($data['name']) ? $data['name'] : null;
$model->email = isset($data['email']) ? $data['email'] : null;
$model->subject = isset($data['subject']) ? $data['subject'] : null;
$model->body = isset($data['body']) ? $data['body'] : null;
~~~
### 安全屬性
塊賦值只應用在模型當前yii\base\Model::scenario場景yii\base\Model::scenarios()方法 列出的稱之為?*安全屬性*?的屬性上,例如,如果`User`模型申明以下場景, 當當前場景為`login`時候,只有`username`?and?`password`?可被塊賦值,其他屬性不會被賦值。
~~~
public function scenarios()
{
return [
'login' => ['username', 'password'],
'register' => ['username', 'email', 'password'],
];
}
~~~
> 補充: 塊賦值只應用在安全屬性上,因為你想控制哪些屬性會被終端用戶輸入數據所修改, 例如,如果?`User`?模型有一個`permission`屬性對應用戶的權限, 你可能只想讓這個屬性在后臺界面被管理員修改。
由于默認yii\base\Model::scenarios()的實現會返回yii\base\Model::rules()所有屬性和數據, 如果不覆蓋這個方法,表示所有只要出現在活動驗證規則中的屬性都是安全的。
為此,提供一個特別的別名為?`safe`?的驗證器來申明哪些屬性是安全的不需要被驗證, 如下示例的規則申明?`title`?和?`description`都為安全屬性。
~~~
public function rules()
{
return [
[['title', 'description'], 'safe'],
];
}
~~~
### 非安全屬性
如上所述,yii\base\Model::scenarios() 方法提供兩個用處:定義哪些屬性應被驗證,定義哪些屬性安全。 在某些情況下,你可能想驗證一個屬性但不想讓他是安全的,可在`scenarios()`方法中屬性名加一個驚嘆號?`!`。 例如像如下的`secret`屬性。
~~~
public function scenarios()
{
return [
'login' => ['username', 'password', '!secret'],
];
}
~~~
當模型在?`login`?場景下,三個屬性都會被驗證,但只有?`username`和?`password`?屬性會被塊賦值, 要對`secret`屬性賦值,必須像如下例子明確對它賦值。
~~~
$model->secret = $secret;
~~~
## 數據導出
模型通常要導出成不同格式,例如,你可能想將模型的一個集合轉成JSON或Excel格式, 導出過程可分解為兩個步驟,第一步,模型轉換成數組;第二步,數組轉換成所需要的格式。 你只需要關注第一步,因為第二步可被通用的數據轉換器如yii\web\JsonResponseFormatter來完成。
將模型轉換為數組最簡單的方式是使用 yii\base\Model::attributes 屬性,例如:
~~~
$post = \app\models\Post::findOne(100);
$array = $post->attributes;
~~~
yii\base\Model::attributes 屬性會返回?*所有*?yii\base\Model::attributes() 申明的屬性的值。
更靈活和強大的將模型轉換為數組的方式是使用 yii\base\Model::toArray() 方法, 它的行為默認和 yii\base\Model::attributes 相同, 但是它允許你選擇哪些稱之為*字段*的數據項放入到結果數組中并同時被格式化。 實際上,它是導出模型到 RESTful 網頁服務開發的默認方法,詳情請參閱[響應格式](http://www.yiichina.com/doc/guide/2.0/rest-response-formatting).
### 字段
字段是模型通過調用yii\base\Model::toArray()生成的數組的單元名。
默認情況下,字段名對應屬性名,但是你可以通過覆蓋 yii\base\Model::fields() 和/或 yii\base\Model::extraFields() 方法來改變這種行為, 兩個方法都返回一個字段定義列表,`fields()`?方法定義的字段是默認字段,表示`toArray()`方法默認會返回這些字段。`extraFields()`方法定義額外可用字段,通過`toArray()`方法指定`$expand`參數來返回這些額外可用字段。 例如如下代碼會返回`fields()`方法定義的所有字段和`extraFields()`方法定義的`prettyName`?and?`fullAddress`字段。
~~~
$array = $model->toArray([], ['prettyName', 'fullAddress']);
~~~
可通過覆蓋?`fields()`?來增加、刪除、重命名和重定義字段,`fields()`?方法返回值應為數組, 數組的鍵為字段名,數組的值為對應的可為屬性名或匿名函數返回的字段定義對應的值。 特使情況下,如果字段名和屬性定義名相同,可以省略數組鍵,例如:
~~~
// 明確列出每個字段,特別用于你想確保數據表或模型屬性改變不會導致你的字段改變(保證后端的API兼容).
public function fields()
{
return [
// 字段名和屬性名相同
'id',
// 字段名為 "email",對應屬性名為 "email_address"
'email' => 'email_address',
// 字段名為 "name", 值通過PHP代碼返回
'name' => function () {
return $this->first_name . ' ' . $this->last_name;
},
];
}
// 過濾掉一些字段,特別用于你想繼承父類實現并不想用一些敏感字段
public function fields()
{
$fields = parent::fields();
// 去掉一些包含敏感信息的字段
unset($fields['auth_key'], $fields['password_hash'], $fields['password_reset_token']);
return $fields;
}
~~~
> 警告:由于模型的所有屬性會被包含在導出數組,最好檢查數據確保沒包含敏感數據, 如果有敏感數據,應覆蓋?`fields()`?方法過濾掉,在上述列子中,我們選擇過濾掉?`auth_key`,?`password_hash`?and?`password_reset_token`。
## 最佳實踐
模型是代表業務數據、規則和邏輯的中心地方,通常在很多地方重用, 在一個設計良好的應用中,模型通常比[控制器](http://www.yiichina.com/doc/guide/2.0/structure-controllers)代碼多。
歸納起來,模型
* 可包含屬性來展示業務數據;
* 可包含驗證規則確保數據有效和完整;
* 可包含方法實現業務邏輯;
* 不應直接訪問請求,session和其他環境數據,這些數據應該由[控制器](http://www.yiichina.com/doc/guide/2.0/structure-controllers)傳入到模型;
* 應避免嵌入HTML或其他展示代碼,這些代碼最好在?[視圖](http://www.yiichina.com/doc/guide/2.0/structure-views)中處理;
* 單個模型中避免太多的?[場景](http://www.yiichina.com/doc/guide/2.0/structure-models#scenarios).
在開發大型復雜系統時應經常考慮最后一條建議, 在這些系統中,模型會很大并在很多地方使用,因此會包含需要規則集和業務邏輯, 最后維護這些模型代碼成為一個噩夢,因為一個簡單修改會影響好多地方, 為確保模型好維護,最好使用以下策略:
* 定義可被多個?[應用主體](http://www.yiichina.com/doc/guide/2.0/structure-applications)?或?[模塊](http://www.yiichina.com/doc/guide/2.0/structure-modules)?共享的模型基類集合。 這些模型類應包含通用的最小規則集合和邏輯。
* 在每個使用模型的?[應用主體](http://www.yiichina.com/doc/guide/2.0/structure-applications)?或?[模塊](http://www.yiichina.com/doc/guide/2.0/structure-modules)中, 通過繼承對應的模型基類來定義具體的模型類,具體模型類包含應用主體或模塊指定的規則和邏輯。
例如,在[高級應用模板](http://www.yiichina.com/doc/guide/2.0/tutorial-advanced-app),你可以定義一個模型基類`common\models\Post`, 然后在前臺應用中,定義并使用一個繼承`common\models\Post`的具體模型類`frontend\models\Post`, 在后臺應用中可以類似地定義`backend\models\Post`。 通過這種策略,你清楚`frontend\models\Post`只對應前臺應用,如果你修改它,就無需擔憂修改會影響后臺應用。
- 介紹(Introduction)
- 關于 Yii(About Yii)
- 從 Yii 1.1 升級(Upgrading from Version 1.1)
- 入門(Getting Started)
- 安裝 Yii(Installing Yii)
- 運行應用(Running Applications)
- 第一次問候(Saying Hello)
- 使用 Forms(Working with Forms)
- 玩轉 Databases(Working with Databases)
- 用 Gii 生成代碼(Generating Code with Gii)
- 更上一層樓(Looking Ahead)
- 應用結構(Application Structure)
- 結構概述(Overview)
- 入口腳本(Entry Scripts)
- 應用(Applications)
- 應用組件(Application Components)
- 控制器(Controllers)
- 模型(Models)
- 視圖(Views)
- 模塊(Modules)
- 過濾器(Filters)
- 小部件(Widgets)
- 前端資源(Assets)
- 擴展(Extensions)
- 請求處理(Handling Requests)
- 運行概述(Overview)
- 引導(Bootstrapping)
- 路由引導與創建 URL(Routing and URL Creation)
- 請求(Requests)
- 響應(Responses)
- Sessions and Cookies
- 錯誤處理(Handling Errors)
- 日志(Logging)
- 關鍵概念(Key Concepts)
- 組件(Components)
- 屬性(Properties)
- 事件(Events)
- 行為(Behaviors)
- 配置(Configurations)
- 別名(Aliases)
- 類自動加載(Class Autoloading)
- 服務定位器(Service Locator)
- 依賴注入容器(Dependency Injection Container)
- 配合數據庫工作(Working with Databases)
- 數據庫訪問(Data Access Objects): 數據庫連接、基本查詢、事務和模式操作
- 查詢生成器(Query Builder): 使用簡單抽象層查詢數據庫
- 活動記錄(Active Record): 活動記錄對象關系映射(ORM),檢索和操作記錄、定義關聯關系
- 數據庫遷移(Migrations): 在團體開發中對你的數據庫使用版本控制
- Sphinx
- Redis
- MongoDB
- ElasticSearch
- 接收用戶數據(Getting Data from Users)
- 創建表單(Creating Forms)
- 輸入驗證(Validating Input)
- 文件上傳(Uploading Files)
- 收集列表輸入(Collecting Tabular Input)
- 多模型同時輸入(Getting Data for Multiple Models)
- 顯示數據(Displaying Data)
- 格式化輸出數據(Data Formatting)
- 分頁(Pagination)
- 排序(Sorting)
- 數據提供器(Data Providers)
- 數據小部件(Data Widgets)
- 操作客戶端腳本(Working with Client Scripts)
- 主題(Theming)
- 安全(Security)
- 認證(Authentication)
- 授權(Authorization)
- 處理密碼(Working with Passwords)
- 客戶端認證(Auth Clients)
- 安全領域的最佳實踐(Best Practices)
- 緩存(Caching)
- 概述(Overview)
- 數據緩存(Data Caching)
- 片段緩存(Fragment Caching)
- 分頁緩存(Page Caching)
- HTTP 緩存(HTTP Caching)
- RESTful Web 服務
- 快速入門(Quick Start)
- 資源(Resources)
- 控制器(Controllers)
- 路由(Routing)
- 格式化響應(Response Formatting)
- 授權驗證(Authentication)
- 速率限制(Rate Limiting)
- 版本化(Versioning)
- 錯誤處理(Error Handling)
- 開發工具(Development Tools)
- 調試工具欄和調試器(Debug Toolbar and Debugger)
- 使用 Gii 生成代碼(Generating Code using Gii)
- TBD 生成 API 文檔(Generating API Documentation)
- 測試(Testing)
- 概述(Overview)
- 搭建測試環境(Testing environment setup)
- 單元測試(Unit Tests)
- 功能測試(Functional Tests)
- 驗收測試(Acceptance Tests)
- 測試夾具(Fixtures)
- 高級專題(Special Topics)
- 高級應用模版(Advanced Project Template)
- 從頭構建自定義模版(Building Application from Scratch)
- 控制臺命令(Console Commands)
- 核心驗證器(Core Validators)
- 國際化(Internationalization)
- 收發郵件(Mailing)
- 性能優化(Performance Tuning)
- 共享主機環境(Shared Hosting Environment)
- 模板引擎(Template Engines)
- 集成第三方代碼(Working with Third-Party Code)
- 小部件(Widgets)
- Bootstrap 小部件(Bootstrap Widgets)
- jQuery UI 小部件(jQuery UI Widgets)
- 助手類(Helpers)
- 助手一覽(Overview)
- Array 助手(ArrayHelper)
- Html 助手(Html)
- Url 助手(Url)