모델은 MVC 패턴의 일부이며 비즈니스 데이터, 규칙 및 논리를 나타내는 객체입니다.
모델은 CModel 또는 해당 하위 클래스의 인스턴스입니다. 모델은 데이터 및 이와 관련된 비즈니스 논리를 보유하는 데 사용됩니다.
모델은 별도의 데이터 개체입니다. 데이터 테이블의 행일 수도 있고 사용자가 입력한 양식일 수도 있습니다. 데이터 객체의 각 필드는 모델의 속성에 해당합니다. 각 속성에는 라벨이 있으며 일련의 규칙을 통해 확인할 수 있습니다.
Yii는 폼 모델과 액티브 레코드라는 두 가지 모델을 구현합니다. 둘 다 동일한 기본 클래스 CModel에서 상속됩니다.
양식 모델은 CFormModel의 인스턴스입니다. 양식 모델은 사용자 입력에서 얻은 데이터를 보유하는 데 사용됩니다. 이 데이터는 종종 획득, 사용 및 폐기됩니다. 예를 들어 로그인 페이지에서 양식 모델을 사용하여 최종 사용자가 제공한 사용자 이름과 비밀번호 정보를 나타낼 수 있습니다.
AR(Active Record)은 객체 지향 스타일로 데이터베이스 액세스를 추상화하기 위한 디자인 패턴입니다. 각 AR 개체는 CActiveRecord의 인스턴스 또는 해당 하위 클래스 중 하나입니다. 데이터 테이블의 행을 나타냅니다. 행의 필드는 AR 개체의 속성에 해당합니다.
모델 클래스는 yiibaseModel 또는 해당 하위 클래스를 상속하여 정의할 수 있습니다. 기본 클래스 yiibaseModel은 다음과 같은 많은 실용적인 기능을 지원합니다.
속성
모델은 속성을 통해 비즈니스 데이터를 나타냅니다. 각 속성은 모델의 공개적으로 액세스 가능한 속성과 유사하며 모델이 소유한 속성을 지정합니다.
모델의 속성은 객체의 속성과 마찬가지로 액세스할 수 있습니다.
$model = new \app\models\ContactForm; // "name" 是ContactForm模型的属性 $model->name = 'example'; echo $model->name;
yiibaseModel의 ArrayAccess 및 ArrayIterator 배열 반복자 지원 덕분에 속성은 배열 셀 항목처럼 액세스할 수도 있습니다.
$model = new \app\models\ContactForm; // 像访问数组单元项一样访问属性 $model['name'] = 'example'; echo $model['name']; // 迭代器遍历模型 foreach ($model as $name => $value) { echo "$name: $value\n"; }
속성 정의
기본적으로 모델 클래스는 yiibaseModel에서 직접 상속되며 모든 비정적 공용 비정적 공용 멤버 변수는 속성입니다. 예를 들어, 다음 ContactForm 모델 클래스에는 이름, 이메일, 제목 및 본문의 네 가지 속성이 있습니다. ContactForm 모델은 HTML 양식에서 얻은 입력 데이터를 나타내는 데 사용됩니다.
namespace app\models; use yii\base\Model; class ContactForm extends Model { public $name; public $email; public $subject; public $body; }
또 다른 방법은 yiibaseModel::attributes()를 재정의하여 속성을 정의하는 것입니다. 이 메서드는 모델의 속성 이름을 반환합니다. 예를 들어 yiidbActiveRecord는 해당 데이터 테이블 열 이름을 속성 이름으로 반환합니다. 속성을 일반 개체 속성처럼 액세스하려면 __get(), __set()과 같은 매직 메서드를 재정의해야 할 수도 있습니다.
속성 태그
속성을 표시하거나 입력을 받으면 속성 관련 레이블을 표시해야 하는 경우가 많습니다. 예를 들어 속성 이름이 firstName이라고 가정하면 양식 입력이나 오류 메시지 등의 일부 위치에 표시해야 할 수도 있습니다. 최종 사용자에게 더 친숙한 이름 태그입니다.
yiibaseModel::getAttributeLabel()을 호출하여 속성의 레이블을 가져올 수 있습니다. 예를 들면 다음과 같습니다.
$model = new \app\models\ContactForm; // 显示为 "Name" echo $model->getAttributeLabel('name');
기본적으로 속성 레이블은 yiibaseModel::generateAttributeLabel() 메서드를 통해 속성 이름에서 자동으로 생성됩니다. 이는 자동으로 카멜 표기법 변수 이름을 첫 글자가 대문자인 여러 단어로 변환합니다. 예를 들어 사용자 이름은 사용자 이름, firstName으로 변환됩니다. 이름으로 변환합니다.
자동으로 생성된 라벨을 사용하지 않으려면 yiibaseModel::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', ]; } }
애플리케이션이 여러 언어를 지원하는 경우 아래와 같이 속성 레이블을 번역하고 yiibaseModel::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'), ]; }
모델의 시나리오를 사용하여 동일한 속성에 대해 서로 다른 라벨을 반환하는 등 조건에 따라 라벨을 정의할 수도 있습니다.
보충: 속성 레이블은 뷰의 일부이지만 모델에서 레이블을 선언하는 것은 일반적으로 매우 편리하며 매우 간결하고 재사용 가능한 코드로 이어질 수 있습니다.
장면
모델은 여러 시나리오에서 사용될 수 있습니다. 예를 들어 사용자 모듈은 사용자 로그인 입력을 수집하거나 사용자가 등록할 때 사용될 수 있습니다. 다양한 시나리오에서 모델은 다양한 비즈니스 규칙과 논리를 사용할 수 있습니다. 예를 들어 이메일 속성은 등록 중에는 필수이지만 로그인 중에는 필요하지 않습니다.
모델은 yiibaseModel::scenario 속성을 사용하여 사용 시나리오를 추적합니다. 기본적으로 모델은 기본이라는 시나리오를 설정하는 두 가지 방법을 보여줍니다.
// 场景作为属性来设置 $model = new User; $model->scenario = 'login'; // 场景通过构造初始化配置来设置 $model = new User(['scenario' => 'login']);
기본적으로 모델에서 지원하는 시나리오는 모델에 선언된 유효성 검사 규칙에 따라 결정되지만 아래와 같이 yiibaseModel::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 类中.
scenarios() 方法返回一个数组,数组的键为场景名,值为对应的 active attributes活动属性。 活动属性可被 块赋值 并遵循验证规则在上述例子中,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; } }
场景特性主要在验证 和 属性块赋值 中使用。 你也可以用于其他目的,例如可基于不同的场景定义不同的 属性标签。
验证规则
当模型接收到终端用户输入的数据,数据应当满足某种规则(称为 验证规则, 也称为 业务规则)。 例如假定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'], ]; }
一条规则可用来验证一个或多个属性,一个属性可对应一条或多条规则。 更多关于如何申明验证规则的详情请参考 验证输入 一节.
有时你想一条规则只在某个 场景 下应用,为此你可以指定规则的 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 网页服务开发的默认方法,详情请参阅响应格式.
字段
字段是模型通过调用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。
最佳实践
模型是代表业务数据、规则和逻辑的中心地方,通常在很多地方重用, 在一个设计良好的应用中,模型通常比控制器代码多。
归纳起来,模型:
在开发大型复杂系统时应经常考虑最后一条建议, 在这些系统中,模型会很大并在很多地方使用,因此会包含需要规则集和业务逻辑, 最后维护这些模型代码成为一个噩梦,因为一个简单修改会影响好多地方, 为确保模型好维护,最好使用以下策略:
定义可被多个 应用主体 或 模块 共享的模型基类集合。 这些模型类应包含通用的最小规则集合和逻辑。
在每个使用模型的 应用主体 或 模块中, 通过继承对应的模型基类来定义具体的模型类,具体模型类包含应用主体或模块指定的规则和逻辑。
例如,在高级应用模板,你可以定义一个模型基类common\models\Post, 然后在前台应用中,定义并使用一个继承common\models\Post的具体模型类frontend\models\Post, 在后台应用中可以类似地定义backend\models\Post。 通过这种策略,你清楚frontend\models\Post只对应前台应用,如果你修改它,就无需担忧修改会影响后台应用。