©
This document usesPHP Chinese website manualRelease
模型是 MVC 模式中的一部分, 是代表业务数据、规则和逻辑的对象。
可通过继承 yii\base\Model 或它的子类定义模型类,基类yii\base\Model支持许多实用的特性:
Model
类也是更多高级模型如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 数组访问 和 ArrayIterator 数组迭代器:
$model=new\app\models\ContactForm;// 像访问数组单元项一样访问属性$model['name'] ='example';echo$model['name'];// 迭代器遍历模型foreach($modelas$name=>$value) {echo"$name: $value\n"; }
默认情况下你的模型类直接从yii\base\Model继承,所有non-static public非静态公有成员变量都是属性。 例如,下述ContactForm
模型类有四个属性name
,email
,subject
andbody
,ContactForm
模型用来代表从HTML表单获取的输入数据。
namespaceapp\models;useyii\base\Model;classContactFormextendsModel{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() 方法明确指定属性标签,例如:
namespaceapp\models;useyii\base\Model;classContactFormextendsModel{public$name;public$email;public$subject;public$body;publicfunctionattributeLabels(){return['name'=>'Your name','email'=>'Your email address','subject'=>'Subject','body'=>'Content', ]; } }
应用支持多语言的情况下,可翻译属性标签, 可在 yii\base\Model::attributeLabels() 方法中定义,如下所示:
publicfunctionattributeLabels(){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场景, 可对相同的属性返回不同的标签。
补充:属性标签是 视图一部分,但是在模型中申明标签通常非常方便,并可行程非常简洁可重用代码。
模型可能在多个场景下使用,例如User
模块可能会在收集用户登录输入,也可能会在用户注册时使用。 在不同的场景下,模型可能会使用不同的业务规则和逻辑,例如email
属性在注册时强制要求有,但在登陆时不需要。
模型使用 yii\base\Model::scenario 属性保持使用场景的跟踪, 默认情况下,模型支持一个名为default
的场景,如下展示两种设置场景的方法:
// 场景作为属性来设置$model=newUser;$model->scenario ='login';// 场景通过构造初始化配置来设置$model=newUser(['scenario'=>'login']);
默认情况下,模型支持的场景由模型中申明的 验证规则 来决定, 但你可以通过覆盖yii\base\Model::scenarios()方法来自定义行为,如下所示:
namespaceapp\models;useyii\db\ActiveRecord;classUserextendsActiveRecord{publicfunctionscenarios(){return['login'=> ['username','password'],'register'=> ['username','email','password'], ]; } }
补充:在上述和下述的例子中,模型类都是继承yii\db\ActiveRecord, 因为多场景的使用通常发生在Active Record 类中.
scenarios()
方法返回一个数组,数组的键为场景名,值为对应的active attributes活动属性。 活动属性可被 块赋值 并遵循验证规则 在上述例子中,username
和password
在login
场景中启用,在register
场景中, 除了username
andpassword
外email
也被启用。
scenarios()
方法默认实现会返回所有yii\base\Model::rules()方法申明的验证规则中的场景, 当覆盖scenarios()
时,如果你想在默认场景外使用新场景,可以编写类似如下代码:
namespaceapp\models;useyii\db\ActiveRecord;classUserextendsActiveRecord{publicfunctionscenarios(){$scenarios=parent::scenarios();$scenarios['login'] = ['username','password'];$scenarios['register'] = ['username','email','password'];return$scenarios; } }
场景特性主要在验证 和 属性块赋值 中使用。 你也可以用于其他目的,例如可基于不同的场景定义不同的 属性标签。
当模型接收到终端用户输入的数据,数据应当满足某种规则(称为验证规则, 也称为业务规则)。 例如假定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
模型申明的验证规则:
publicfunctionrules(){return[// name, email, subject 和 body 属性必须有值[['name','email','subject','body'],'required'],// email 属性必须是一个有效的电子邮箱地址['email','email'], ]; }
一条规则可用来验证一个或多个属性,一个属性可对应一条或多条规则。 更多关于如何申明验证规则的详情请参考 验证输入 一节.
有时你想一条规则只在某个 场景 下应用,为此你可以指定规则的on
属性,如下所示:
publicfunctionrules(){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
andpassword
可被块赋值,其他属性不会被赋值。
publicfunctionscenarios(){return['login'=> ['username','password'],'register'=> ['username','email','password'], ]; }
补充: 块赋值只应用在安全属性上,因为你想控制哪些属性会被终端用户输入数据所修改, 例如,如果
User
模型有一个permission
属性对应用户的权限, 你可能只想让这个属性在后台界面被管理员修改。
由于默认yii\base\Model::scenarios()的实现会返回yii\base\Model::rules()所有属性和数据, 如果不覆盖这个方法,表示所有只要出现在活动验证规则中的属性都是安全的。
为此,提供一个特别的别名为safe
的验证器来申明哪些属性是安全的不需要被验证, 如下示例的规则申明title
和description
都为安全属性。
publicfunctionrules(){return[ [['title','description'],'safe'], ]; }
如上所述,yii\base\Model::scenarios() 方法提供两个用处:定义哪些属性应被验证,定义哪些属性安全。 在某些情况下,你可能想验证一个属性但不想让他是安全的,可在scenarios()
方法中属性名加一个惊叹号!
。 例如像如下的secret
属性。
publicfunctionscenarios(){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 网页服务开发的默认方法,详情请参阅响应格式.
字段是模型通过调用yii\base\Model::toArray()生成的数组的单元名。
默认情况下,字段名对应属性名,但是你可以通过覆盖 yii\base\Model::fields() 和/或 yii\base\Model::extraFields() 方法来改变这种行为, 两个方法都返回一个字段定义列表,fields()
方法定义的字段是默认字段,表示toArray()
方法默认会返回这些字段。extraFields()
方法定义额外可用字段,通过toArray()
方法指定$expand
参数来返回这些额外可用字段。 例如如下代码会返回fields()
方法定义的所有字段和extraFields()
方法定义的prettyName
andfullAddress
字段。
$array=$model->toArray([], ['prettyName','fullAddress']);
可通过覆盖fields()
来增加、删除、重命名和重定义字段,fields()
方法返回值应为数组, 数组的键为字段名,数组的值为对应的可为属性名或匿名函数返回的字段定义对应的值。 特使情况下,如果字段名和属性定义名相同,可以省略数组键,例如:
// 明确列出每个字段,特别用于你想确保数据表或模型属性改变不会导致你的字段改变(保证后端的API兼容).publicfunctionfields(){return[// 字段名和属性名相同'id',// 字段名为 "email",对应属性名为 "email_address"'email'=>'email_address',// 字段名为 "name", 值通过PHP代码返回'name'=>function(){return$this->first_name .' '.$this->last_name; }, ]; }// 过滤掉一些字段,特别用于你想继承父类实现并不想用一些敏感字段publicfunctionfields(){$fields=parent::fields();// 去掉一些包含敏感信息的字段unset($fields['auth_key'],$fields['password_hash'],$fields['password_reset_token']);return$fields; }
警告:由于模型的所有属性会被包含在导出数组,最好检查数据确保没包含敏感数据, 如果有敏感数据,应覆盖
fields()
方法过滤掉,在上述列子中,我们选择过滤掉auth_key
,password_hash
andpassword_reset_token
。
模型是代表业务数据、规则和逻辑的中心地方,通常在很多地方重用, 在一个设计良好的应用中,模型通常比控制器代码多。
归纳起来,模型
在开发大型复杂系统时应经常考虑最后一条建议, 在这些系统中,模型会很大并在很多地方使用,因此会包含需要规则集和业务逻辑, 最后维护这些模型代码成为一个噩梦,因为一个简单修改会影响好多地方, 为确保模型好维护,最好使用以下策略:
common\models\Post
, 然后在前台应用中,定义并使用一个继承
common\models\Post
的具体模型类
frontend\models\Post
, 在后台应用中可以类似地定义
backend\models\Post
。 通过这种策略,你清楚
frontend\models\Post
只对应前台应用,如果你修改它,就无需担忧修改会影响后台应用。