structure-models.md 17.7 KB
Newer Older
funson86 committed
1
模型
2 3
======

funson86 committed
4 5
模型是 [MVC](http://en.wikipedia.org/wiki/Model%E2%80%93view%E2%80%93controller) 模式中的一部分,
是代表业务数据、规则和逻辑的对象。
6

funson86 committed
7
可通过继承 [[yii\base\Model]] 或它的子类定义模型类,基类[[yii\base\Model]]支持许多实用的特性:
8

funson86 committed
9 10 11 12 13
* [属性](#attributes): 代表可像普通类属性或数组一样被访问的业务数据;
* [属性标签](#attribute-labels): 指定属性显示出来的标签;
* [块赋值](#massive-assignment): 支持一步给许多属性赋值;
* [验证规则](#validation-rules): 确保输入数据符合所申明的验证规则;
* [数据导出](#data-exporting): 允许模型数据导出为自定义格式的数组。
14

funson86 committed
15 16
`Model` 类也是更多高级模型如[Active Record 活动记录](db-active-record.md)的基类,
更多关于这些高级模型的详情请参考相关手册。
17

funson86 committed
18
> 补充:模型并不强制一定要继承[[yii\base\Model]],但是由于很多组件支持[[yii\base\Model]],最好使用它做为模型基类。
19 20


funson86 committed
21
## 属性 <a name="attributes"></a>
22

funson86 committed
23 24
模型通过 *属性* 来代表业务数据,每个属性像是模型的公有可访问属性,
[[yii\base\Model::attributes()]] 指定模型所拥有的属性。
25

funson86 committed
26
可像访问一个对象属性一样访问模型的属性:
27 28 29 30

```php
$model = new \app\models\ContactForm;

funson86 committed
31
// "name" 是ContactForm模型的属性
32 33 34 35
$model->name = 'example';
echo $model->name;
```

funson86 committed
36 37
也可像访问数组单元项一样访问属性,这要感谢[[yii\base\Model]]支持 [ArrayAccess 数组访问](http://php.net/manual/en/class.arrayaccess.php) 
[ArrayIterator 数组迭代器](http://php.net/manual/en/class.arrayiterator.php):
38 39 40 41

```php
$model = new \app\models\ContactForm;

funson86 committed
42
// 像访问数组单元项一样访问属性
43 44 45
$model['name'] = 'example';
echo $model['name'];

funson86 committed
46
// 迭代器遍历模型
47 48 49 50 51 52
foreach ($model as $name => $value) {
    echo "$name: $value\n";
}
```


funson86 committed
53
### 定义属性 <a name="defining-attributes"></a>
54

funson86 committed
55 56 57
默认情况下你的模型类直接从[[yii\base\Model]]继承,所有 *non-static public非静态公有* 成员变量都是属性。
例如,下述`ContactForm` 模型类有四个属性`name`, `email`, `subject` and `body`
`ContactForm` 模型用来代表从HTML表单获取的输入数据。
58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73

```php
namespace app\models;

use yii\base\Model;

class ContactForm extends Model
{
    public $name;
    public $email;
    public $subject;
    public $body;
}
```


funson86 committed
74 75 76
另一种方式是可覆盖 [[yii\base\Model::attributes()]] 来定义属性,该方法返回模型的属性名。
例如 [[yii\db\ActiveRecord]] 返回对应数据表列名作为它的属性名,
注意可能需要覆盖魔术方法如`__get()`, `__set()`使属性像普通对象属性被访问。
77 78


funson86 committed
79
### 属性标签 <a name="attribute-labels"></a>
80

funson86 committed
81 82
当属性显示或获取输入时,经常要显示属性相关标签,例如假定一个属性名为`firstName`
在某些地方如表单输入或错误信息处,你可能想显示对终端用户来说更友好的 `First Name` 标签。
83

funson86 committed
84
可以调用 [[yii\base\Model::getAttributeLabel()]] 获取属性的标签,例如:
85 86 87 88

```php
$model = new \app\models\ContactForm;

funson86 committed
89
// 显示为 "Name"
90 91 92
echo $model->getAttributeLabel('name');
```

funson86 committed
93 94 95
默认情况下,属性标签通过[[yii\base\Model::generateAttributeLabel()]]方法自动从属性名生成. 
它会自动将驼峰式大小写变量名转换为多个首字母大写的单词,例如 `username` 转换为 `Username`
`firstName` 转换为 `First Name`
96

funson86 committed
97
如果你不想用自动生成的标签,可以覆盖 [[yii\base\Model::attributeLabels()]] 方法明确指定属性标签,例如:
98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122

```php
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',
        ];
    }
}
```

funson86 committed
123 124
应用支持多语言的情况下,可翻译属性标签,
可在 [[yii\base\Model::attributeLabels()|attributeLabels()]] 方法中定义,如下所示:
125 126 127 128 129 130 131 132 133 134 135 136 137

```php
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'),
    ];
}
```

funson86 committed
138 139
甚至可以根据条件定义标签,例如通过使用模型的 [scenario场景](#scenarios)
可对相同的属性返回不同的标签。
140

funson86 committed
141
> 补充:属性标签是 [视图](structure-views.md)一部分,但是在模型中申明标签通常非常方便,并可行程非常简洁可重用代码。
142 143


funson86 committed
144
## 场景 <a name="scenarios"></a>
145

funson86 committed
146 147
模型可能在多个 *场景* 下使用,例如 `User` 模块可能会在收集用户登录输入,也可能会在用户注册时使用。
在不同的场景下,模型可能会使用不同的业务规则和逻辑,例如 `email` 属性在注册时强制要求有,但在登陆时不需要。
148

funson86 committed
149 150
模型使用 [[yii\base\Model::scenario]] 属性保持使用场景的跟踪,
默认情况下,模型支持一个名为 `default` 的场景,如下展示两种设置场景的方法:
151 152

```php
funson86 committed
153
// 场景作为属性来设置
154 155 156
$model = new User;
$model->scenario = 'login';

funson86 committed
157
// 场景通过构造初始化配置来设置
158 159 160
$model = new User(['scenario' => 'login']);
```

funson86 committed
161 162
默认情况下,模型支持的场景由模型中申明的 [验证规则](#validation-rules) 来决定,
但你可以通过覆盖[[yii\base\Model::scenarios()]]方法来自定义行为,如下所示:
163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180

```php
namespace app\models;

use yii\db\ActiveRecord;

class User extends ActiveRecord
{
    public function scenarios()
    {
        return [
            'login' => ['username', 'password'],
            'register' => ['username', 'email', 'password'],
        ];
    }
}
```

funson86 committed
181 182
> 补充:在上述和下述的例子中,模型类都是继承[[yii\db\ActiveRecord]],
  因为多场景的使用通常发生在[Active Record](db-active-record.md) 类中.
183

funson86 committed
184 185 186 187
`scenarios()` 方法返回一个数组,数组的键为场景名,值为对应的 *active attributes活动属性*
活动属性可被 [块赋值](#massive-assignment) 并遵循[验证规则](#validation-rules)
在上述例子中,`username``password``login`场景中启用,在 `register` 场景中, 
除了 `username` and `password``email` 也被启用。
188

funson86 committed
189 190
`scenarios()` 方法默认实现会返回所有[[yii\base\Model::rules()]]方法申明的验证规则中的场景,
当覆盖`scenarios()`时,如果你想在默认场景外使用新场景,可以编写类似如下代码:
191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208

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

funson86 committed
209 210
场景特性主要在[验证](#validation-rules)[属性块赋值](#massive-assignment) 中使用。
你也可以用于其他目的,例如可基于不同的场景定义不同的 [属性标签](#attribute-labels)
211 212


funson86 committed
213
## 验证规则 <a name="validation-rules"></a>
214

funson86 committed
215 216 217
当模型接收到终端用户输入的数据,数据应当满足某种规则(称为 *验证规则*, 也称为 *业务规则*)。
例如假定`ContactForm`模型,你可能想确保所有属性不为空且 `email` 属性包含一个有效的邮箱地址,
如果某个属性的值不满足对应的业务规则,相应的错误信息应显示,以帮助用户修正错误。
218

funson86 committed
219 220 221
可调用 [[yii\base\Model::validate()]] 来验证接收到的数据,
该方法使用[[yii\base\Model::rules()]]申明的验证规则来验证每个相关属性,
如果没有找到错误,会返回 true,否则它会将错误保存在 [[yii\base\Model::errors]] 属性中并返回false,例如:
222 223 224 225

```php
$model = new \app\models\ContactForm;

funson86 committed
226
// 用户输入数据赋值到模型属性
227 228 229
$model->attributes = \Yii::$app->request->post('ContactForm');

if ($model->validate()) {
funson86 committed
230
    // 所有输入数据都有效 all inputs are valid
231
} else {
funson86 committed
232
    // 验证失败:$errors 是一个包含错误信息的数组
233 234 235 236 237
    $errors = $model->errors;
}
```


funson86 committed
238 239
通过覆盖 [[yii\base\Model::rules()]] 方法指定模型属性应该满足的规则来申明模型相关验证规则。
下述例子显示`ContactForm`模型申明的验证规则:
240 241 242 243 244

```php
public function rules()
{
    return [
funson86 committed
245
        // name, email, subject 和 body 属性必须有值
246 247
        [['name', 'email', 'subject', 'body'], 'required'],

funson86 committed
248
        // email 属性必须是一个有效的电子邮箱地址
249 250 251 252 253
        ['email', 'email'],
    ];
}
```

funson86 committed
254 255
一条规则可用来验证一个或多个属性,一个属性可对应一条或多条规则。
更多关于如何申明验证规则的详情请参考 [验证输入](input-validation.md) 一节.
256

funson86 committed
257
有时你想一条规则只在某个 [场景](#scenarios) 下应用,为此你可以指定规则的 `on` 属性,如下所示:
258 259 260 261 262

```php
public function rules()
{
    return [
funson86 committed
263
        // 在"register" 场景下 username, email 和 password 必须有值
264 265
        [['username', 'email', 'password'], 'required', 'on' => 'register'],

funson86 committed
266
        // 在 "login" 场景下 username 和 password 必须有值
267 268 269 270 271
        [['username', 'password'], 'required', 'on' => 'login'],
    ];
}
```

funson86 committed
272 273
如果没有指定 `on` 属性,规则会在所有场景下应用, 在当前[[yii\base\Model::scenario|scenario]]
下应用的规则称之为 *active rule活动规则*
274

funson86 committed
275
一个属性只会属于`scenarios()`中定义的活动属性且在`rules()`申明对应一条或多条活动规则的情况下被验证。
276 277


funson86 committed
278
## 块赋值 <a name="massive-assignment"></a>
279

funson86 committed
280 281 282 283
块赋值只用一行代码将用户所有输入填充到一个模型,非常方便,
它直接将输入数据对应填充到 [[yii\base\Model::attributes]] 属性。
以下两段代码效果是相同的,都是将终端用户输入的表单数据赋值到 `ContactForm` 模型的属性,
明显地前一段块赋值的代码比后一段代码简洁且不易出错。
284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299

```php
$model = new \app\models\ContactForm;
$model->attributes = \Yii::$app->request->post('ContactForm');
```

```php
$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;
```


funson86 committed
300
### 安全属性 <a name="safe-attributes"></a>
301

funson86 committed
302 303 304
块赋值只应用在模型当前[[yii\base\Model::scenario|scenario]]场景[[yii\base\Model::scenarios()]]方法
列出的称之为 *安全属性* 的属性上,例如,如果`User`模型申明以下场景,
当当前场景为`login`时候,只有`username` and `password` 可被块赋值,其他属性不会被赋值。
305 306 307 308 309 310 311 312 313 314 315

```php
public function scenarios()
{
    return [
        'login' => ['username', 'password'],
        'register' => ['username', 'email', 'password'],
    ];
}
```

funson86 committed
316 317 318
> 补充: 块赋值只应用在安全属性上,因为你想控制哪些属性会被终端用户输入数据所修改,
  例如,如果 `User` 模型有一个`permission`属性对应用户的权限,
  你可能只想让这个属性在后台界面被管理员修改。
319

funson86 committed
320 321
由于默认[[yii\base\Model::scenarios()]]的实现会返回[[yii\base\Model::rules()]]所有属性和数据,
如果不覆盖这个方法,表示所有只要出现在活动验证规则中的属性都是安全的。
322

funson86 committed
323 324
为此,提供一个特别的别名为 `safe` 的验证器来申明哪些属性是安全的不需要被验证,
如下示例的规则申明 `title``description` 都为安全属性。
325 326 327 328 329 330 331 332 333 334 335

```php
public function rules()
{
    return [
        [['title', 'description'], 'safe'],
    ];
}
```


funson86 committed
336
### 非安全属性 <a name="unsafe-attributes"></a>
337

funson86 committed
338 339 340
如上所述,[[yii\base\Model::scenarios()]] 方法提供两个用处:定义哪些属性应被验证,定义哪些属性安全。
在某些情况下,你可能想验证一个属性但不想让他是安全的,可在`scenarios()`方法中属性名加一个惊叹号 `!`
例如像如下的`secret`属性。
341 342 343 344 345 346 347 348 349 350

```php
public function scenarios()
{
    return [
        'login' => ['username', 'password', '!secret'],
    ];
}
```

funson86 committed
351 352
当模型在 `login` 场景下,三个属性都会被验证,但只有 `username``password` 属性会被块赋值,
要对`secret`属性赋值,必须像如下例子明确对它赋值。
353 354 355 356 357 358

```php
$model->secret = $secret;
```


funson86 committed
359
## 数据导出 <a name="data-exporting"></a>
360

funson86 committed
361 362 363
模型通常要导出成不同格式,例如,你可能想将模型的一个集合转成JSON或Excel格式,
导出过程可分解为两个步骤,第一步,模型转换成数组;第二步,数组转换成所需要的格式。
你只需要关注第一步,因为第二步可被通用的数据转换器如[[yii\web\JsonResponseFormatter]]来完成。
364

funson86 committed
365
将模型转换为数组最简单的方式是使用 [[yii\base\Model::attributes]] 属性,例如:
366 367 368 369 370 371

```php
$post = \app\models\Post::findOne(100);
$array = $post->attributes;
```

funson86 committed
372
[[yii\base\Model::attributes]] 属性会返回 *所有* [[yii\base\Model::attributes()]] 申明的属性的值。
373

funson86 committed
374 375 376 377
更灵活和强大的将模型转换为数组的方式是使用 [[yii\base\Model::toArray()]] 方法,
它的行为默认和 [[yii\base\Model::attributes]] 相同,
但是它允许你选择哪些称之为*字段*的数据项放入到结果数组中并同时被格式化。
实际上,它是导出模型到 RESTful 网页服务开发的默认方法,详情请参阅[响应格式](rest-response-formatting.md).
378 379


funson86 committed
380
### 字段 <a name="fields"></a>
381

funson86 committed
382
字段是模型通过调用[[yii\base\Model::toArray()]]生成的数组的单元名。
383

funson86 committed
384 385 386 387 388
默认情况下,字段名对应属性名,但是你可以通过覆盖
[[yii\base\Model::fields()|fields()]] 和/或 [[yii\base\Model::extraFields()|extraFields()]] 方法来改变这种行为,
两个方法都返回一个字段定义列表,`fields()` 方法定义的字段是默认字段,表示`toArray()`方法默认会返回这些字段。 
`extraFields()`方法定义额外可用字段,通过`toArray()`方法指定`$expand`参数来返回这些额外可用字段。
例如如下代码会返回`fields()`方法定义的所有字段和`extraFields()`方法定义的`prettyName` and `fullAddress`字段。
389 390 391 392 393

```php
$array = $model->toArray([], ['prettyName', 'fullAddress']);
```

funson86 committed
394 395 396
可通过覆盖 `fields()` 来增加、删除、重命名和重定义字段,`fields()` 方法返回值应为数组,
数组的键为字段名,数组的值为对应的可为属性名或匿名函数返回的字段定义对应的值。
特使情况下,如果字段名和属性定义名相同,可以省略数组键,例如:
397 398

```php
funson86 committed
399
// 明确列出每个字段,特别用于你想确保数据表或模型属性改变不会导致你的字段改变(保证后端的API兼容).
400 401 402
public function fields()
{
    return [
funson86 committed
403
        // 字段名和属性名相同
404 405
        'id',

funson86 committed
406
        // 字段名为 "email",对应属性名为 "email_address"
407 408
        'email' => 'email_address',

funson86 committed
409
        // 字段名为 "name", 值通过PHP代码返回
410 411 412 413 414 415
        'name' => function () {
            return $this->first_name . ' ' . $this->last_name;
        },
    ];
}

funson86 committed
416
// 过滤掉一些字段,特别用于你想继承父类实现并不想用一些敏感字段
417 418 419 420
public function fields()
{
    $fields = parent::fields();

funson86 committed
421
    // 去掉一些包含敏感信息的字段
422 423 424 425 426 427
    unset($fields['auth_key'], $fields['password_hash'], $fields['password_reset_token']);

    return $fields;
}
```

funson86 committed
428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 453 454 455 456 457 458 459 460
> 警告:由于模型的所有属性会被包含在导出数组,最好检查数据确保没包含敏感数据,
> 如果有敏感数据,应覆盖 `fields()` 方法过滤掉,在上述列子中,我们选择过滤掉
> `auth_key`, `password_hash` and `password_reset_token`。


## 最佳实践 <a name="best-practices"></a>

模型是代表业务数据、规则和逻辑的中心地方,通常在很多地方重用,
在一个设计良好的应用中,模型通常比[控制器](structure-controllers.md)代码多。

归纳起来,模型

* 可包含属性来展示业务数据;
* 可包含验证规则确保数据有效和完整;
* 可包含方法实现业务逻辑;
* 不应直接访问请求,session和其他环境数据,这些数据应该由[控制器](structure-controllers.md)传入到模型;
* 应避免嵌入HTML或其他展示代码,这些代码最好在 [视图](structure-views.md)中处理;
* 单个模型中避免太多的 [场景](#scenarios).

在开发大型复杂系统时应经常考虑最后一条建议,
在这些系统中,模型会很大并在很多地方使用,因此会包含需要规则集和业务逻辑,
最后维护这些模型代码成为一个噩梦,因为一个简单修改会影响好多地方,
为确保模型好维护,最好使用以下策略:

* 定义可被多个 [应用主体](structure-applications.md)[模块](structure-modules.md) 共享的模型基类集合。
  这些模型类应包含通用的最小规则集合和逻辑。
* 在每个使用模型的 [应用主体](structure-applications.md)[模块](structure-modules.md)中,
  通过继承对应的模型基类来定义具体的模型类,具体模型类包含应用主体或模块指定的规则和逻辑。

例如,在[高级应用模板](tutorial-advanced-app.md),你可以定义一个模型基类`common\models\Post`
然后在前台应用中,定义并使用一个继承`common\models\Post`的具体模型类`frontend\models\Post`
在后台应用中可以类似地定义`backend\models\Post`
通过这种策略,你清楚`frontend\models\Post`只对应前台应用,如果你修改它,就无需担忧修改会影响后台应用。