directory search
阅读前篇 简介 Yii 是什么 从 Yii 1.1 升级 入门 安装 Yii 运行应用 第一次问候 使用Forms 数据库应用 使用 Gii 生成代码 进阶 应用结构 概述 入口脚本 应用(Applications) 应用组件(Application Components) 控制器(Controllers) 模型(Models) 视图(views) 模块(Modules) 过滤器(Filters) 小部件(Widgets) 前端资源(Assets) 扩展(Extensions) 请求处理 运行概述 启动引导(Bootstrapping) 路由和创建URL 请求(Requests) 响应(Responses) Sessions 和 Cookies 错误处理(Handling Errors) 日志(Logging) 关键概念 组件(Component) 属性(Property) 事件(Events) 行为(Behaviors) 配置(Configurations) 别名(Aliases) 类自动加载(Autoloading) 服务定位器(Service Locator) 依赖注入容器(Dependency Injection Container) 配合数据库工作 数据库访问 (Data Access Objects) 查询生成器(Query Builder) 活动记录(Active Record) 数据库迁移(Migrations) Sphinx Redis MongoDB Elasticsearch 接收用户数据 创建表单(Creating Forms) 输入验证(Validating Input) 文件上传(Uploading Files) 收集列表输入(Collecting Tabular Input) 多模型的复合表单(Getting Data for Multiple Models) 显示数据 格式化输出数据(Data Formatting) 分页(Pagination) 排序(Sorting) 数据提供器(Data Providers) 数据小部件(Data Widgets) 客户端脚本使用(Working with Client Scripts) 主题(Theming) 安全 认证(Authentication) 授权(Authorization) 处理密码(Working with Passwords) 客户端认证(Auth Clients) 最佳安全实践(Best Practices) 缓存 概述 数据缓存 片段缓存 页面缓存 HTTP 缓存 RESTfull Web服务 快速入门(Quick Start) 资源(Resources) 控制器(Controllers) 路由(Routing) 格式化响应(Response Formatting) 授权认证(Authentication) 速率限制(Rate Limiting) 版本(Versioning) 错误处理(Error Handling) 开发工具 调试工具栏和调试器 使用Gii生成代码 生成API文档 测试 概述(Overview) 配置测试环境(Testing environment setup) 单元测试(Unit Tests) 功能测试(Function Tests) 验收测试(Acceptance Tests) 测试夹具(Fixtures) 高级专题 高级应用模板 创建自定义应用程序结构 控制台命令 核心验证器(Core Validators) 国际化 收发邮件 性能优化 共享主机环境 模板引擎 集成第三方代码 小部件 Bootstrap 小部件 Jquery UI 助手类 概述 Array 助手(ArrayHelper) Html 助手(Html) Url 助手(Url)
characters

模型

模型

模型是 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,subjectandbodyContactForm模型用来代表从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转换为UsernamefirstName转换为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活动属性。 活动属性可被 块赋值 并遵循验证规则 在上述例子中,usernamepasswordlogin场景中启用,在register场景中, 除了usernameandpasswordemail也被启用。

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时候,只有usernameandpassword可被块赋值,其他属性不会被赋值。

publicfunctionscenarios(){return['login'=> ['username','password'],'register'=> ['username','email','password'], ]; }

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

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

为此,提供一个特别的别名为safe的验证器来申明哪些属性是安全的不需要被验证, 如下示例的规则申明titledescription都为安全属性。

publicfunctionrules(){return[ [['title','description'],'safe'], ]; }

非安全属性

如上所述,yii\base\Model::scenarios() 方法提供两个用处:定义哪些属性应被验证,定义哪些属性安全。 在某些情况下,你可能想验证一个属性但不想让他是安全的,可在scenarios()方法中属性名加一个惊叹号!。 例如像如下的secret属性。

publicfunctionscenarios(){return['login'=> ['username','password','!secret'], ]; }

当模型在login场景下,三个属性都会被验证,但只有usernamepassword属性会被块赋值, 要对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()方法定义的prettyNameandfullAddress字段。

$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_hashandpassword_reset_token

最佳实践

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

归纳起来,模型

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

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

  • 定义可被多个 应用主体 或 模块 共享的模型基类集合。 这些模型类应包含通用的最小规则集合和逻辑。
  • 在每个使用模型的 应用主体 或 模块中, 通过继承对应的模型基类来定义具体的模型类,具体模型类包含应用主体或模块指定的规则和逻辑。
例如,在高级应用模板,你可以定义一个模型基类 common\models\Post, 然后在前台应用中,定义并使用一个继承 common\models\Post的具体模型类 frontend\models\Post, 在后台应用中可以类似地定义 backend\models\Post。 通过这种策略,你清楚 frontend\models\Post只对应前台应用,如果你修改它,就无需担忧修改会影响后台应用。
Previous article: Next article: