- 后端
- Web框架
【免费下载链接】yii2
Yii 2: The Fast, Secure and Professional PHP Framework
Model是 Yii2 框架中 MVC 架构的核心组件,承载业务数据、业务规则与业务逻辑。本指南以官方文档 structure-models.md 为主线,结合framework/base/Model.php、framework/base/ArrayableTrait.php的源码实现与 ModelTest.php 测试用例,系统讲解如何定义模型属性、声明属性标签、使用多场景(Scenario)、编写验证规则(Validation Rules)、执行批量赋值(Massive Assignment)以及将模型导出为数组。读完本文,你将掌握 Yii2 模型层的完整用法,并能从源码层面理解"安全属性""活跃属性""字段(Field)"等关键概念背后的设计动机,从而写出更安全、更易维护的模型代码。
模型在 MVC 架构中的定位
模型是 MVC(Model-View-Controller) 架构的一部分,它们是代表业务数据、业务规则和业务逻辑的对象。在 Yii2 中,你可以通过继承[[yii\base\Model]]或其子类来创建模型类。基类yii\base\Model提供了以下开箱即用的能力:
- 属性(Attributes):代表业务数据,既可以像普通对象属性一样访问,也可以像数组元素一样访问;
- 属性标签(Attribute Labels):为属性指定在界面展示时使用的友好名称;
- 批量赋值(Massive Assignment):用一行代码同时填充多个属性;
- 验证规则(Validation Rules):基于声明的规则确保输入数据的有效性;
- 数据导出(Data Exporting):将模型数据按可定制的格式导出为数组。
Model类同时也是更高级模型(如 Active Record)的基类。[[yii\base\Model]]并不强制要求所有模型都必须继承它,但由于 Yii2 的众多组件(表单组件、验证器、数据提供器等)都围绕它构建,官方强烈建议将其作为模型的默认基类。
Info:本文及官方文档中的示例大量使用
yii\db\ActiveRecord子类,是因为"多场景"等高级用法通常出现在 Active Record 类中;但本文讲述的所有机制(属性、标签、场景、验证、批量赋值、导出)在普通yii\base\Model子类上同样适用。
属性(Attributes)
属性的本质与访问方式
模型通过属性(Attribute)表示业务数据,每个属性都相当于模型的一个公开可访问的成员。方法[[yii\base\Model::attributes()]]定义了模型类拥有哪些属性。
属性可以像普通对象属性一样访问:
$model = new \app\models\ContactForm; // "name" 是 ContactForm 的一个属性 $model->name = 'example'; echo $model->name;得益于yii\base\Model对 ArrayAccess 中的getIterator()、offsetExists()、offsetGet()、offsetSet()、offsetUnset()),属性还可以像数组元素一样读写,并且模型可以直接被foreach遍历:
$model = new \app\models\ContactForm; // 以数组元素方式访问属性 $model['name'] = 'example'; echo $model['name']; // 遍历模型的所有属性 foreach ($model as $name => $value) { echo "$name: $value\n"; }如何定义属性
默认情况下,如果你的模型类直接继承自yii\base\Model,那么该类中所有非静态的 public 成员变量都会成为属性。例如下面的ContactForm模型拥有四个属性:name、email、subject和body,它通常用于表示从 HTML 表单接收的输入数据:
namespace app\models; use yii\base\Model; class ContactForm extends Model { public $name; public $email; public $subject; public $body; }查看源码 Model.php 可以确认这一默认行为:attributes()通过 PHP 反射(ReflectionClass::getProperties(ReflectionProperty::IS_PUBLIC))遍历类的所有 public 属性,并过滤掉静态属性:
public function attributes() { $class = new ReflectionClass($this); $names = []; foreach ($class->getProperties(\ReflectionProperty::IS_PUBLIC) as $property) { if (!$property->isStatic()) { $names[] = $property->getName(); } } return $names; }你也可以重写attributes()以其他方式定义属性:该方法只需返回模型中的属性名列表。例如yii\db\ActiveRecord就是通过返回关联数据库表的列名作为属性名来实现的(即"列即属性")。注意,如果以这种方式定义属性,通常还需要重写__get()和__set()等魔术方法,使属性可以像普通对象属性一样被访问。
属性标签(Attribute Labels)
在展示属性值或为属性收集输入时,往往需要显示与属性关联的标签。例如,属性名为firstName,你可能希望在表单输入框和错误消息中展示对终端用户更友好的First Name。
调用[[yii\base\Model::getAttributeLabel()]]可以获取属性标签:
$model = new \app\models\ContactForm; // 显示 "Name" echo $model->getAttributeLabel('name');标签的自动生成
默认情况下,属性标签由属性名自动生成,生成逻辑位于[[yii\base\Model::generateAttributeLabel()]]。源码 Model.php 显示它实际委托给Inflector::camel2words($name, true):将 camelCase 风格的变量名拆分为多个单词,并把每个单词首字母大写。例如username变成Username,firstName变成First Name,department_name也会变成Department Name。
getAttributeLabel()的完整逻辑(Model.php)是:先在attributeLabels()的返回数组中查找显式声明,若未找到才回退到自动生成:
public function getAttributeLabel($attribute) { $labels = $this->attributeLabels(); return isset($labels[$attribute]) ? $labels[$attribute] : $this->generateAttributeLabel($attribute); }显式声明标签
如果不希望使用自动生成的标签,可以重写[[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', ]; } }多语言标签
对于支持多语言的应用程序,可以直接在attributeLabels()中使用\Yii::t()进行国际化翻译(完整的 i18n 机制参见 tutorial-i18n.md 或 guide 下的英文版):
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)为同一个属性返回不同的标签。
Info:严格来说,属性标签属于视图(Views) 的职责范畴。但在模型中声明标签通常非常方便,且能使代码更简洁、更易复用,因此成为 Yii2 的主流实践。
场景(Scenarios)
一个模型可能被用于不同的场景。例如User模型既可用于收集用户登录输入,也可用于用户注册。在不同场景下,模型可能使用不同的业务规则与业务逻辑——比如email属性在注册时是必填的,在登录时却不是。
模型通过[[yii\base\Model::scenario]]属性跟踪它当前所处的场景。默认情况下,模型只支持一个名为default的场景(即常量SCENARIO_DEFAULT)。设置场景有两种等价方式:
// 方式一:作为属性设置 $model = new User; $model->scenario = 'login'; // 方式二:通过构造函数配置 $model = new User(['scenario' => 'login']);为便于维护,官方英文版文档推荐用类常量替代字符串字面量,例如User::SCENARIO_LOGIN(定义常量const SCENARIO_LOGIN = 'login';)。
场景的默认推导逻辑
默认情况下,模型支持的场景由模型中声明的验证规则推导而来。查看源码 Model.php 中scenarios()的默认实现可以看到完整推导流程:
- 初始化
default场景; - 遍历所有验证器(
getValidators()),收集每个验证器on/except属性中出现的场景名; - 对每个验证器,根据其
on(仅指定场景生效)、except(排除指定场景)或两者皆空(所有场景生效)三种情况,把验证器关联的属性分别归入对应场景; - 最终返回
场景名 => 活跃属性数组的映射。
因此,只要你通过rules()声明了带on的规则,对应场景就会被自动创建,这正是"默认场景由验证规则决定"的源码依据。
重写 scenarios()
你可以重写[[yii\base\Model::scenarios()]]来定制场景及其活跃属性(Active Attributes)。scenarios()返回一个数组,键为场景名,值为该场景下对应的活跃属性。活跃属性可以被批量赋值,并且需要接受验证。例如:
namespace app\models; use yii\db\ActiveRecord; class User extends ActiveRecord { const SCENARIO_LOGIN = 'login'; const SCENARIO_REGISTER = 'register'; public function scenarios() { return [ self::SCENARIO_LOGIN => ['username', 'password'], self::SCENARIO_REGISTER => ['username', 'email', 'password'], ]; } }上述例子中,username和password是login场景的活跃属性;而在register场景中,email也是活跃属性。
在重写scenarios()时,如果你想在默认场景之外新增场景(而不是完全替换默认实现推导出的场景),需要先调用父类实现再合并,例如:
namespace app\models; use yii\db\ActiveRecord; class User extends ActiveRecord { const SCENARIO_LOGIN = 'login'; const SCENARIO_REGISTER = 'register'; public function scenarios() { $scenarios = parent::scenarios(); $scenarios[self::SCENARIO_LOGIN] = ['username', 'password']; $scenarios[self::SCENARIO_REGISTER] = ['username', 'email', 'password']; return $scenarios; } }场景机制主要用于验证和批量赋值,但也可用于其他目的,例如根据当前场景返回不同的属性标签。
验证规则(Validation Rules)
当模型的数据来自终端用户时,必须经过验证以确保其满足特定规则(即验证规则,也称业务规则)。例如对于ContactForm模型,你可能希望确保所有属性均非空,且email属性是合法的电子邮件地址。当某些属性的值不满足对应业务规则时,应显示合适的错误消息帮助用户修正。
validate() 的调用与流程
调用[[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()) { // 所有输入均有效 } else { // 验证失败:$errors 是包含错误消息的数组 $errors = $model->errors; }查看 validate() 的源码 可以理解完整的执行链路:
- 默认先调用
clearErrors()清空旧错误(可通过$clearErrors = false关闭); - 触发
beforeValidate事件(beforeValidate(),若返回false则验证中止); - 获取当前场景并检查它是否存在于
scenarios()中,若当前场景未知,会抛出InvalidArgumentException(对应测试 testValidateWithUnknownScenario); - 若未指定属性名,则取当前场景的
activeAttributes(); - 遍历
getActiveValidators()返回的活跃验证器,逐个调用$validator->validateAttributes($this, $attributeNames); - 触发
afterValidate事件; - 返回
!$this->hasErrors()。
验证错误可通过getErrors()(返回二维数组:属性 => 错误消息数组)、getFirstErrors()(每个属性仅第一条错误)和getFirstError($attribute)获取。
声明验证规则
要声明与模型关联的验证规则,需要重写[[yii\base\Model::rules()]],返回模型属性必须满足的规则。下面的示例展示了为ContactForm声明的验证规则:
public function rules() { return [ // name、email、subject 和 body 均为必填 [['name', 'email', 'subject', 'body'], 'required'], // email 必须是合法的电子邮件地址 ['email', 'email'], ]; }一条规则可以校验一个或多个属性,一个属性也可以被一条或多条规则校验。关于如何声明各种验证规则(内建验证器、行内验证器、自定义验证器、when条件等),详见 input-validation.md。
从源码 createValidators() 可以看出,rules()返回的每条规则都会被转换为一个yii\validators\Validator对象:规则必须是Validator实例,或满足"[0]为属性、[1]为验证器类型"的数组格式,否则抛出InvalidConfigException。子类若需继承父类的规则,应使用array_merge()合并父类规则。
按场景限定规则
有时你希望某条规则只在特定场景中生效。此时可以为规则指定on属性:
public function rules() { return [ // username、email 和 password 在 "register" 场景中均为必填 [['username', 'email', 'password'], 'required', 'on' => 'register'], // username 和 password 在 "login" 场景中均为必填 [['username', 'password'], 'required', 'on' => 'login'], // 未指定 on 的规则在所有场景中生效 [['username'], 'string'], ]; }如果未指定on属性,规则将在所有场景中生效。能够应用于当前场景的规则称为"活跃规则(Active Rule)"。
结合scenarios()的源码可知,属性只有在同时满足以下两个条件时才会被验证:
- 它是
scenarios()中当前场景声明的活跃属性; - 它与
rules()中一条或多条活跃规则关联。
批量赋值(Massive Assignment)
批量赋值是一种用一行代码将用户输入填充到模型中的便捷方式。它通过把输入数据直接赋给[[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;除了直接给$attributes赋值,更常用的做法是调用[[yii\base\Model::load()]](源码)。load()会根据formName()返回的表单名(默认是类名去掉命名空间的短类名,见 formName() 源码)从$_POST/$_GET等数据数组中取出对应子数组并执行批量赋值,内部同样经过setAttributes()的安全检查:
if ($model->load(\Yii::$app->request->post()) && $model->validate()) { // 处理成功提交 }此外还有loadMultiple()(批量填充多个模型,常用于表格行输入)和validateMultiple()(批量验证多个模型)两个静态工具方法,均位于 Model.php。
安全属性(Safe Attributes)
批量赋值只作用于所谓的"安全属性(Safe Attributes)"——即[[yii\base\Model::scenarios()]]中当前场景所列出的属性。例如,如果User模型声明了如下场景,那么当当前场景为login时,只有username和password可以被批量赋值,其他所有属性都会被忽略:
public function scenarios() { return [ self::SCENARIO_LOGIN => ['username', 'password'], self::SCENARIO_REGISTER => ['username', 'email', 'password'], ]; }Info:批量赋值只作用于安全属性的原因是:你需要控制哪些属性可以被终端用户数据修改。例如,如果
User模型有一个决定用户权限的permission属性,你必然希望该属性只能由管理员通过后台界面修改,而绝不能让普通用户通过表单提交来篡改。
由于scenarios()的默认实现会从rules()中推导出所有场景及属性,只要属性出现在某条活跃验证规则中,它默认就是安全的。setAttributes()的源码清楚地展示了这一机制:默认$safeOnly = true,仅当属性名存在于safeAttributes()返回值中时才被赋值;否则调用onUnsafeAttribute()(源码,在YII_DEBUG开启时会记录调试日志)。
对应地,safeAttributes()的源码会跳过!前缀的属性并返回安全属性列表;activeAttributes()的源码则返回当前场景下需要验证的活跃属性(会去掉!前缀)。这两个方法的分工正是"验证哪些属性"与"允许批量赋值哪些属性"两件事的分离点。
为此,框架专门提供了别名为safe的特殊验证器,让你可以把属性声明为安全属性而不实际验证它。例如下面的规则把title和description都声明为安全属性:
public function rules() { return [ [['title', 'description'], 'safe'], ]; }isAttributeSafe($attribute)(源码)可用于在代码中判断某属性当前是否安全。
非安全属性(Unsafe Attributes)
如前所述,scenarios()方法承担两个职责:决定哪些属性需要验证、决定哪些属性是安全的。在少数情况下,你可能希望验证某个属性但又不把它标记为安全属性。此时可以在scenarios()中为该属性名加上感叹号前缀!,如下例中的secret属性:
public function scenarios() { return [ self::SCENARIO_LOGIN => ['username', 'password', '!secret'], ]; }当模型处于login场景时,三个属性都会被验证;但只有username和password可以被批量赋值。要为secret属性赋值,必须显式地写:
$model->secret = $secret;同样的技巧也可以在rules()中实现:
public function rules() { return [ [['username', 'password', '!secret'], 'required', 'on' => 'login'], ]; }此时username、password和secret均为必填,但secret必须显式赋值。相关行为在测试 testSetAttributesUnsafeIsIgnored、testIsAttributeSafe 与 testActiveAttributes 中均有覆盖。
数据导出(Data Exporting)
模型经常需要导出为各种格式,例如把一组模型转换为 JSON 或 Excel。导出过程可以拆分为两个相互独立的步骤:
- 将模型转换为数组;
- 将数组转换为目标格式。
你只需关注第一步,因为第二步可以由通用的数据格式化器完成,例如yii\web\JsonResponseFormatter。
使用 $attributes 属性导出
把模型转换为数组的最简单方式是使用[[yii\base\Model::$attributes]]属性:
$post = \app\models\Post::findOne(100); $array = $post->attributes;默认情况下,$attributes属性会返回attributes()声明的所有属性的值(对应源码getAttributes(),Model.php)。
使用 toArray() 导出
更灵活、更强大的方式是调用[[yii\base\Model::toArray()]]。它的默认行为与$attributes一致,但它允许你选择将哪些数据项(称为"字段",Field)放入结果数组,以及如何格式化它们。事实上,它是 RESTful Web 服务开发中导出模型的默认方式(见 rest-response-formatting.md)。
toArray()的实现在yii\base\ArrayableTrait中(ArrayableTrait.php),核心流程是:
- 通过
resolveFields()把请求的字段与fields()/extraFields()的声明合并解析为"字段名 => 定义"映射(resolveFields() 源码); - 若定义是字符串,则取该属性值;若是闭包则调用它;
- 若
$recursive为true,嵌套的Arrayable对象会被递归转换为数组(字段名支持用点号item.field嵌套选择)。
字段(Fields)
字段(Field)就是调用toArray()所得到数组中的一个具名元素。
默认情况下,字段名与属性名一一对应。但你可以通过重写[[yii\base\Model::fields()]]和/或[[yii\base\Model::extraFields()]]来改变这一行为。两者都应返回字段定义列表:
fields()定义的是默认字段,即toArray()默认返回的字段;extraFields()定义的是额外可用字段,只有通过$expand参数显式指定时才会被返回。
例如下面的代码会返回fields()定义的所有字段,再加上extraFields()中定义的prettyName和fullAddress字段(若存在):
$array = $model->toArray([], ['prettyName', 'fullAddress']);重写fields()可以新增、删除、重命名或重新定义字段。fields()的返回值必须是数组:键为字段名,值为对应的字段定义——可以是属性名,也可以是返回字段值的匿名函数。特殊情况下,当字段名与定义它的属性名一致时,可以省略数组键。例如:
// 显式列出每个字段——最适合用于确保数据库表或模型属性的变更不会导致 API 字段变化(保持向后兼容) public function fields() { return [ // 字段名与属性名相同 'id', // 字段名为 "email",对应的属性名为 "email_address" 'email' => 'email_address', // 字段名为 "name",其值由匿名函数定义 '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()的默认实现(Model.php)返回attributes()并以同名索引,即默认导出全部属性;extraFields()的默认实现(ArrayableTrait.php)返回空数组。此外,从 Model.php 的 fields() 文档注释 可以看到,你还可以根据场景或当前用户权限等上下文信息返回不同的字段集合,例如为普通用户过滤掉敏感字段。
Warning:因为默认情况下模型的所有属性都会被包含在导出的数组中,你必须检查你的数据,确保其中不包含敏感信息。如果存在此类信息,应重写
fields()将其过滤掉。上例中就特意过滤了auth_key、password_hash和password_reset_token三个字段。
最佳实践(Best Practices)
模型是表示业务数据、业务规则和业务逻辑的核心位置,它们经常需要在不同地方被复用。在设计良好的应用中,模型通常比控制器更"胖"。
总体而言,模型应该:
- 可以包含用于表示业务数据的属性;
- 可以包含用于确保数据有效性和完整性的验证规则;
- 可以包含实现业务逻辑的方法;
- 不应该直接访问请求(Request)、会话(Session)或其他环境数据——这些数据应由控制器注入模型;
- 应该避免内嵌 HTML 或其他表现层代码——这更适合放在视图(Views) 中;
- 应该避免在单个模型中定义过多场景。
最后一条建议尤其适用于大型复杂系统:在这些系统中,模型可能因为被多处使用而变得非常"胖",包含大量规则和业务逻辑,这往往导致维护噩梦——一次小小的代码改动就可能影响到多个不同的地方。为了让模型代码更易维护,可以采取如下策略:
- 定义一组被不同应用(Applications)或模块(Modules)共享的基础模型类。这些基础模型类应包含所有使用场景中共同的最小规则集和逻辑集;
- 在每个使用该模型的应用或模块中,定义继承自对应基础模型类的具体模型类。具体模型类只包含该应用或模块特有的规则和逻辑。
例如,在 Yii2 高级项目模板(Advanced Project Template)中,你可以定义一个基础模型类common\models\Post;然后为 front-end 应用定义并使用继承自它的具体模型类frontend\models\Post;类似地,为 back-end 应用定义backend\models\Post。采用这种策略,你可以确信frontend\models\Post中的代码只对 front-end 应用有效,修改它时不必担心会破坏 back-end 应用。
源码与测试对照:进一步阅读
本文所有结论均可直接在仓库中验证,建议按以下路径深入:
- 模型基类实现:framework/base/Model.php(
attributes()、attributeLabels()、scenarios()、rules()、validate()、setAttributes()、safeAttributes()、activeAttributes()、fields()、load()等全部核心方法); - 数组导出实现:framework/base/ArrayableTrait.php(
toArray()、fields()、extraFields()、resolveFields())与 framework/base/Arrayable.php 接口; - 测试用例:tests/framework/base/ModelTest.php(涵盖
testSetAttributes、testActiveAttributes、testIsAttributeSafe、testValidateWithUnknownScenario、testFields、testSetAttributesUnsafeIsIgnored、testValidateMultiple等关键行为)以及 tests/framework/base/DynamicModelTest.php(yii\base\DynamicModel动态模型,见 framework/base/DynamicModel.php,可在不定义类的情况下临时创建带验证规则的模型,适合在控制器中做轻量数据校验); - 进阶主题:验证器全览见 tutorial-core-validators.md,表单输入处理见 input-validation.md,基于表的模型见 db-active-record.md,REST 响应导出见 rest-response-formatting.md。
掌握属性、场景、验证与导出这四大机制,你就抓住了 Yii2 模型层的核心。它们协同工作,共同构成了 Yii2 中"数据入(批量赋值 + 验证)与数据出(导出与序列化)"的完整闭环,也是后续理解 Active Record、表单组件(yii\widgets\ActiveForm)与 REST 控制器的前提。
- 后端
- Web框架
【免费下载链接】yii2
Yii 2: The Fast, Secure and Professional PHP Framework
相关推荐
Yii2 模型(Model)完全指南:属性、场景、验证、批量赋值与数据导出的源码级实战详解
Yii2 模型(Model)完全指南:属性、场景、验证、批量赋值与数据导出的源码级实战详解 导读 本文基于 Yii2 官方指南的「Models(模型)」章节(
后端Web框架Yii 2 模型(Model)完全指南:属性、场景、验证与数据导出的源码级解析
Yii 2 模型(Model)完全指南:属性、场景、验证与数据导出的源码级解析 模型(Model)是 Yii 2 MVC 架构中承载业务数据、规则与逻辑的核心类
后端Web框架Yii 2 模型(Model)完全指南:属性、场景、验证规则与数据导出的深度解析
Yii 2 模型(Model)完全指南:属性、场景、验证规则与数据导出的深度解析 本文以 docs/guide ja/structure models.md h
后端Web框架
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考