阿江 2017-10-08 16:53:03 3126次浏览 0条回复 2 1 0

说明

学习Yii Framework 2易2框架的过程是漫长的也是充满乐趣的以下是我学习Yii2框架时对官网英文资料(请参见原文网址)的翻译和代码实现提供了较完整的代码供你参考不妥之处请多多指正

原文网址:

http://www.yiiframework.com/doc-2.0/guide-structure-models.html

本文主题:模型(Models)

模型(Models)是MVC架构的一部分,它们是表示业务数据、规则和逻辑的对象。

你可以通过继承yii\base\Model类或其子类来创建模型类,基类yii\base\Model支持以下特性: Attributes(属性):代表业务数据,它们可以象普通对象一样存取,也可以象数据元素一样进行操作。 Attribute labels(属性标签):定义属性要显示的标签。 Massive assignment(块赋值):使用一步就可以加载多个属性。 Validation rules(验证规则):确保输入数据通过声明的规则验证。 Data Exporting(数据导出):将模型数据被导出为自定义格式的数组。

模型类也是很多高级模型的基类,如Active Record,请参考高级模型的相关文件以获取更多信息。 http://www.yiiframework.com/doc-2.0/guide-db-active-record.html

信息:尽管你无需将你的模型类都基于yii\base\Model,但因为有很多Yii组件都已支持yii\base\Model,所以将其视为模型的基类是更加明智的选择。

1、Attributes

模型使用属性(attributes)来表示业务数据,每个属性就象模型的一个成员变量一样,yii\base\Model::attributes()方法定义了模型类具有哪些属性。 你可以像存取普通对象的成员变量一样获取属性:

$model=new \app\models\ContactForm;
//"name"是ContactForm的一个属性
$model->name='example';
echo $model->name;

你也可以象获取数组元素一样获取属性,这要归功于yii\base\Model实现了两个PHP接口:ArrayAccess和Traversable,

class Model extends Component implements IteratorAggregate, ArrayAccess, Arrayable{
}
$model=new \app\models\ContactForm;
//象数组一样获取属性
$model['name']='example';
echo $model['name'];
//Model实现traversable,所以可以使用foreach
foreach($mdoel as $name=>$value){
	echo "$name:$value\n";
}
定义属性(Defining Attributes)

默认情况下,如果你的模型类直接扩展自yii\base\Model,那么它的非静态成员变量都是属性,例如:下例中的ContactForm模型类有4个属性:name,email,subject和body,ContactForm模型用于表示从HTML表单中获取的输入数据。

namespace app\models;
use yii\base\Model;
class ContactForm extends Model{
	public $name;
	public $email;
	public $subject;
	public $body;
}

你也可以使用另外一个不同的方法重写yii\base\Model::attributes()来定义属性。这个方法将返回模型中的属性名称,例如,yii\db\ActiveRecord通过返回其关联的数据库表列名来表示它的属性名称,注意,你还需要重写魔术方法get(),set(),这样属性可以象对象成员变量一样被获取到。

属性标签(Attribute Labels)

当显示属性值或获取属性输入项时,你常常需要显示属性相关的标签(label),例如,有一个名为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()明确声明属性标签,例如:

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()方法中完成,例如:

	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),你可以为同一个属性返回不同的标签。

信息:严格意义来讲,属性标签是视图的一部分,但是在模型中定义标签已成为一种惯例,也使得代码更加简洁和利于复用。

2、Scenarios(场景)

一个模型可以在不同的场景中使用,例如,一个User模型可以用于收集用户的登录输入信息,它也可以被用于用户注册界面,在不同的场景中,一个模型可以被用于不同的业务规则和逻辑,例如,email属性在用户注册时是必填项,但在用户登录时则不是。

模型使用yii\base\Model::$scenario 属性来表示它所使用的场景,默认情况下,模型通常都有一个名为default的场景,下例代码展示了一个模型设置场景的两种方法:

//以属性方法设置scenario
$model=new User;
$model->scenario=User::	SCENARIO_LOGIN;

//使用配置设置scenario
$model=new User(['scenario'=>SCENARIO_LOGIN]);

默认情况下,一个模型支持的场景由验证规则(validation rules)方法来定义,不管怎样,你都可以通过重写yii\base\Model::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'],
		];
	}
}

信息:在上例和后续例子中,模型类是继承自yii\db\ActiveRecord类,因为在AR类中多场景应用的更多一些。

scenarios()方法返回一个数组,数组键名是场景名称,键值是对应于活动属性(active attributes), 一个活动属性可以被块赋值(massively assigned),并由validation管理。在上例中,username和password属性在login场景中是有效的,在register场景中,除了username和password之外,email也是有效的。

scenarios()的默认实现将返回在验证规则yii\base\Model::rules()声明方法中的所有场景,当重写scenarios()方法时,如果你要在default的基础上添加新的场景,你可以编写如下代码:

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

场景特性主要由验证和块赋值所使用,你也可以用于其他目的,例如,你可以为当前的场景声明不同的属性标签。

3、Validation Rules

当从终端用户处接收模型数据时,这些数据应该被验证以确保符合特定规则(称为验证规则,也被称为业务规则),例如,一个给定的ContactForm模型,你可能想要确定所有属性都不能为空,并且email属性应是一个有效的邮件地址。如果某些属性的值不符合对应的业务规则,相应的错误信息将被显示出来以帮助用户去修改这些错误。 你可以调用yii\base\Model::validate()方法去验证接收到的数据,此方法将使用yii\base\Model::rules()中声明的验证规则去验证每一个相关属性,如果没有发生错误,它将返回true。否则,它将返回false,并将错误信息保存在yii\base\Model::$errors 中,例如:

$model=new \app\mdoels\ContactForm;
//使用用户输入的数据填充模型属性
$model->attributes=\Yii::$app->request->post('ContactForm');
if($model->validate()){
	//所有输入都是有效的
}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'],
	];
}

一个规则可以用于验证一个或多个属性,一个属性也可以被一个或多个规则所验证。请参考输入验证章节获取如何声明验证规则的更多详情: http://www.yiiframework.com/doc-2.0/guide-input-validation.html

有时,你可能会将一条规则仅适用于特定的场景,要实现此目的,你可以在规则中定义on属性,如下例所示:

public function rules(){
	return [
		//在'register'场景中,'username','email','password'都是必填项
		[['username','email','password'],'required','on'=>self::SCENARIO_REGISTER],	
		//在'login'场景中,'username','password'是必填项
		[['username','password'],'required','on'=>self::SENARIO_LOGIN],
	];
}

如果你没有定义on属性,此规则将应用于所有场景。如果一个规则在当前场景中可以被应用就被称为活动规则(active rule)。 只有当一个属性是scenarios()中定义的活动属性,并且在rules()中定义了一个或多个活动规则时,此属性才会被验证。

4、Massive Assignment(块赋值)

块赋值是一个实用的方法,它仅使用一行代码就将用户的输入填充到了模型中去。它通过将输入数据直接赋值给yii\base\Model::$attributes 属性来填充模型的所有属性。下例两段代码是等效的,将终端用户提交的数据赋值给ContactForm的属性,很明显,前者(former)使用块赋值,比后者(latter)更简洁,也更不容易出错。

//former
$model=new \app\models\ContactForm;
$model->attributes=\Yii::$app->request->post('ContactForm');
//latter
$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;
安全属性(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'],
	];
}

信息:块赋值仅应用于安全属性是因为你需要控制哪些属性可以被终端用户数据修改。例如,User模型中有一个permission属性,此属性决定赋给用户的权限,而你只想让系统管理员通过后台来修改此属性的值,此属性就不应该设置为安全属性(不能被终端用户修改)。

yii\base\Model::scenarios()的默认实现将返回yii\base\Model::rules()中定义的所有的场景和属性,如果你不重写些方法,意味着一旦出现在任意一条有效验证规则中时,它就是安全属性了。

正因如此,Yii提供了一个特定的别名为safe的验证器,你可以无需实际验证它,仅用于声明它是安全的(有效的),例如,下例规则声明title和description都是安全属性:

public function rules(){
	return [
		[['title','description'],'safe'],
	];
}
非安全属性(Unsafe Attributes)

如上所述,yii\base\Model::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,passwrod和secret属性是必填项,但是secret必须被明确赋值。

5、Data Exporting(数据导出)

模型经常需要被导出为不同的格式,例如,你可能需要将一个模型集体转为JSON或Excel格式,导出过程可以被分解为两个独立步骤: 1、模型转为数组 2、数组转为目标格式 你可以只关心第1步,因为第2步可以使用通用数据格式化器实现,例如

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 的默认行为。toArray()方法允许你选择数据项,也就是字段(fields),填充到结果数组中,并被格式化。实际上,它是RESTful网站服务开发中导出模型的默认方法,可以参考Response Formatting中的描述: http://www.yiiframework.com/doc-2.0/guide-rest-response-formatting.html

字段(Fields)

字段就是数组中一个简单的命名元素,此数组是调用yii\base\Model::toArray()方法时获取的。 默认情况下,字段名称与属性名称是相同的,你可以重写filelds()或extraFields()方法来更改此默认行为。两个方法都应返回一个定义字段的列表,由fields()定义的的字段是默认字段,也就是toArray()将默认返回这些字段;extraFields()方法定义额外有效的字段,当使用$expand 参数时toArray()将会返回这些字段。例如,以下代码将返回fields()定义的所有字段,如果在extraFields()中定义了prettyName和FullAddress字段也将被返回。

$array=$model->toArray([],['prettyName','fullAddress']);
实例,toArray,将一行AR记录转为数组:

D:\phpwork\basic\views\article\view.php

    $article2=$article->toArray(['_id','title','authInfo','level']);
    echo "<br>id:".$article2["_id"]['$id'];//转为数组后,MongoDB中的_id的正确写法
	var_dump($article2);
测试结果:
/*
id:58a41ee6e4017a7c0c000030
D:\phpwork\basic\views\article\view.php:7:
array (size=4)
  '_id' => 
    array (size=1)
      '$id' => string '58a41ee6e4017a7c0c000030' (length=24)
  'authInfo' => 
    array (size=3)
      'id' => 
        array (size=1)
          '$id' => string '58a14587e4017a1c1600002a' (length=24)
      'name' => string 'abc' (length=3)
      'level' => string '18' (length=2)
  'title' => string 'Hello Yii2' (length=10)
  'level' => int 14
*/

你可以重写fields()去添加、删除、重命名或重定义字段。fields()返回值将是一个数组,数组键名是字段名称,键值是对应字段的定义,此定义可以是成员变量/属性名或返回对应值的匿名函数。在特定情况下,当一个字段名与属性名相同时,可以省略键名,例如:

public function fields(){
	return [
		'id',
		'email'=>'email_address',
		'name'=>function(){
			return $this->first_name.' '.$this->last_name;
		},
	];
}
public function fields(){
	$fields=parent::fields();
	unset($fields['auth_key'],$fileds['password_hash'],$fields['password_reset_token']);
	return $fields;
}

警告:因为默认情况下,模型的所有属性将被包含在导出数组中,你应该检查你的数据以确保没有包含敏感数据,如果有此类信息,你应该重写fileds()方法将它们过滤出去,在上例中,我们选择性的过滤掉了auth_key,password_hash和password_reset_token。

fields实例:

D:\phpwork\basic\models\ArticleInfo.php

class ArticleInfo extends \yii\mongodb\ActiveRecord{
	......
    public function fields() {
        return ['_id','title'];
    }
    public function extraFields() {
        return ['level','authInfo'];
    }

D:\phpwork\basic\controllers\ArticleController.php

	//输出全部fields(),没有extraFields被输出
	$article2=$article->toArray();
	return $this->render('view',['article'=>$article,'article2'=>$article2]);

D:\phpwork\basic\views\article\view.php

    var_dump($article2);
	//_id的数组获取方法
    echo "<br>id:".$article2["_id"]['$id'];
测试结果:
/*
D:\phpwork\basic\views\article\view.php:6:
array (size=2)
  '_id' => 
    array (size=1)
      '$id' => string '58a41ee6e4017a7c0c000030' (length=24)
  'title' => string 'Hello Yii2' (length=10)

id:58a41ee6e4017a7c0c000030
*/

D:\phpwork\basic\controllers\ArticleController.php

	//输出全部fields(),并输出一个extraFields:authInfo
	$article2=$article->toArray([],['authInfo']);
	return $this->render('view',['article'=>$article,'article2'=>$article2]);

D:\phpwork\basic\views\article\view.php

    var_dump($article2);
	//_id的数组获取方法
    echo "<br>id:".$article2["_id"]['$id'];//获取转为数组的_id
测试结果:
/*
D:\phpwork\basic\views\article\view.php:6:
array (size=3)
  '_id' => 
    array (size=1)
      '$id' => string '58a41ee6e4017a7c0c000030' (length=24)
  'title' => string 'Hello Yii2' (length=10)
  'authInfo' => 
    array (size=3)
      'id' => 
        array (size=1)
          '$id' => string '58a14587e4017a1c1600002a' (length=24)
      'name' => string 'abc' (length=3)
      'level' => string '18' (length=2)

id:58a41ee6e4017a7c0c000030
*/

//输出与上例相同,但子元素保留对象格式: D:\phpwork\basic\controllers\ArticleController.php

	$article2=$article->toArray([],['authInfo'],false);

D:\phpwork\basic\views\article\view.php

	//_id的对象获取方法
	echo "<br>id:".$article2["_id"];//自动调用MongoId的__toString()方法
	var_dump($article2);
测试结果:
/*
id:58a41ee6e4017a7c0c000030
D:\phpwork\basic\views\article\view.php:6:
array (size=3)
  '_id' => 
    object(MongoId)[72]
      public '$id' => string '58a41ee6e4017a7c0c000030' (length=24)
  'title' => string 'Hello Yii2' (length=10)
  'authInfo' => 
    array (size=3)
      'id' => 
        object(MongoId)[73]
          public '$id' => string '58a14587e4017a1c1600002a' (length=24)
      'name' => string 'abc' (length=3)
      'level' => string '18' (length=2)
*/

//只输出_id D:\phpwork\basic\models\ArticleInfo.php

    public function fields() {
        return ['_id','title'];
    }

D:\phpwork\basic\controllers\ArticleController.php

    $article2=$article->toArray(['level','_id']);

D:\phpwork\basic\views\article\view.php

    var_dump($article2);
测试结果:
/*
D:\phpwork\basic\views\article\view.php:6:
array (size=1)
  '_id' => 
    array (size=1)
      '$id' => string '58a41ee6e4017a7c0c000030' (length=24)
*/

D:\phpwork\basic\models\ArticleInfo.php

class ArticleInfo extends \yii\mongodb\ActiveRecord{
    public function fields() {
        return [
            '_id',
			//将AR模型中的title字段赋值给数组,数组属性的名称为topic
            'topic'=>'title',
        ];
    }

D:\phpwork\basic\controllers\ArticleController.php

        $article2=$article->toArray();
        return $this->render('view',['article'=>$article,'article2'=>$article2]);

D:\phpwork\basic\views\article\view.php

    var_dump($article2);
测试结果:
/*
D:\phpwork\basic\views\article\view.php:6:
array (size=2)
  '_id' => 
    array (size=1)
      '$id' => string '58a41ee6e4017a7c0c000030' (length=24)
  'topic' => string 'Hello Yii2' (length=10)
*/
6、Best Practices

模型处于业务数据、规则和逻辑的核心地位,它们通常会在不同的地方被重用,在一个设计良好的应用中,模型通常远大于控制器(controllers)。 总之,模型: 1、包含代表业务数据的属性 2、包含确保数据有效和完整的验证规则 3、包含实现业务逻辑的方法 4、不应直接获取请示、session或其他环境数据,这些数据应由控制器推送到模型中 5、应避免包括HTML或显示层的代码,这些应在视图中实现 6、避免在一个模型中使用太多的场景

当你开发大型的复杂系统时,应着重考虑一下最后一条建议。在这样的系统中,因为模型被很多地方引用,包含了很多的规则和业务逻辑,所以会非常大。维护这样的代码将是一场噩梦,因为一处代码的修改就会影响到很多不同的地方。要让模型代码可以更好的维护,你应遵循以下策略: 1、定义一系列的基础模型类,这样可以被不同的应用或模块所调用,这些模型类应包含通用的、最少的验证规则和业务逻辑。 2、在每个应用或模块中使用一个模型,应从基类中继承并创建一个具体的模型,这个模型将包含针对应用或模块的验证规则和业务逻辑

例如,在高级模板(Advanced Project Template)中,你可以定义一个基础模型类common\models\Post,对于前端应用,你可以定义和使用一个模型类frontend\models\Post,此类继承自common\models\Post;类似的,对于后端应用,你可以定义backend\models\Post。使用此种策略,你应确定frontend\models\Post中的代码只针对前端应用,当你更改它们时,你无需担心你的修改会影响到后端应用。

(全文完)

觉得很赞
    没有找到数据。
您需要登录后才可以回复。登录 | 立即注册