第二层内功 - 表单模型

  • 作者:KK

  • 发表日期:2017.2.18


已经有许多略有经验的程序员表示“我不愿意再在控制器里堆代码,我觉得应该增加一个逻辑层负责业务逻辑,控制器只管调用和传递中间信息,再由逻辑层调用模型,模型调DB……以便实现逻辑层被多个控制器重用和测试”

我也是支持逻辑层思想的程序员之一,而在Yii2框架中与逻辑层对应的则是由表单模型来实现,也就可以这么通俗地说:Yii2里的逻辑层就是表单模型

基础示例

class LoginForm extends \yii\base\Model{

	public $username = '';
	
	public $password = '';
	
	public function login(){
		$user = \app\model\User::findOne(['username' => $this->username]);
		if(!$user){
			$this->addError('username', '无效的用户名或密码');
			return false;
		}
		if($user->password != md5($password)){
			$this->addError('username', '无效的用户名或密码');
			return false;
		}
		
		return Yii::$app->user->login($user);
	}
}

这是一个打算用在登录功能上的表单模型,其中login方法就是登录的逻辑处理,为了简单演示我省略了用户名和密码长度、类型的校验之类的详细处理

然后表单模型通常都让控制器来调用的,控制器的代码可以这样写:

public function actionLogin(){
	$form = new LoginForm();
	$form->username = Yii::$app->request->post('username');
	$form->username = Yii::$app->request->post('password');

	if($form->login()){
		$errorMessage = current($form->firstErrors)[0];
		//成功的结果响应
	}else{
		//失败的结果响应
	}
}

4个基本要点

使用表单模型要注意下面4个要点:

  1. 要继承yii\base\Model


  2. 一般都提供public方法作为业务逻辑处理的方法(像上面的login方法)

    一个方法处理一个业务请求,可以有多个业务,也就是多个public方法


  3. 业务处理过程中,需要的客户端输入参数默认不直接从$_GET、$_POST数据获取,而是访问自身的属性(像上面的$username和$password两个属性)

    而客户端输入的参数由控制器从GET、POST里获取,让控制器来传递给表单模型

    这样就实现了:表单模型不依赖GET、POST数据,降低了依赖,只要能传来特定参数就能执行业务

    也就实现了这个逻辑层可以用在更多场合(比如控制台、第三方接口、后台管理前台用户、模拟登录等)


  4. 业务处理过程中,如果对输入数据校验失败,报错的办法应该用$this->addError('自定义错误标识', '错误消息');并返回false给控制器

    当然也能返回0呀null呀什么的,只要跟控制器协商好什么表示失败就行了

    重要的是,控制器如果要获取失败的消息提示,可以通过$form->firstError[0]这个代码获取,在入门级的文章里不解释这个东西,知道怎么用就是


自动校验数据

客户端用户要操作应用的业务,比如要登录,就要输入用户名和密码啦,那后端就要校验一下是不是string,长度又是否满足啦,其实这些校验级别的事,表单模型有一套方法可以快速自动完成,例子:

class LoginForm extends \yii\base\Model{

	public $username = '';
	
	public $password = '';
	
	//这个方法是我要说的重点!!!
	public function rules(){
		return [
			['username', 'required'], //第1条校验规则
			['password', 'required'], //第2条校验规则
		];
	}
	
	public function login(){
		$user = \app\model\User::findOne(['username' => $this->username]);
		if(!$user){
			$this->addError('username', '无效的用户名或密码');
			return false;
		}
		if($user->password != md5($password)){
			$this->addError('username', '无效的用户名或密码');
			return false;
		}
		
		return Yii::$app->user->login($user);
	}
}

上面通过rules方法返回了一个数组,这个数组代表了一套校验规则

每个数组元素就是1条规则,其中这里有2条规则,第1条的意思是说“$this的username属性不能没有或为空”,第二条规则就是说password属性也不能为空

此时控制器试下跑这样的代码:

$form = new LoginForm();
var_dump($form->validate()); // false
echo current($form->firstErrors)[0]; // username 不能为空

其实表单模型还有一个validate方法,这个方法会按照rules方法返回的规则进行数据校验(就是校验自己的属性)

上面返回false就是说校验不通过啦,为什么不通过呢?压根就是直接new了模型,可是用户名和密码默认就是空的,所以校验规则就通不过去了;而你试试赋一下非空值就能过了


校验规则的基本要素

其实就像上面的,第1个元素是要校验的属性名称,必须是public的属性

第2个元素就是要求特征,required这个特征表示“必须有的”

public function rules(){
	return [
		[属性名称//规则1
		[属性名称//规则2
		[属性名称//规则3……
	];
}

校验规则并不是只声明required就行的

$form = new LoginForm();
$form->username = [json_encode('Jay')]; //这回赋值了
$form->username = '123456';
var_dump($form->validate()); // true

var_dump($form->login()); // false  重点

validate是通过了,因为数据都非空嘛,但login还是返回了false,因为username必须是个字符串,而上面却传了个数组进去!

所以说校验规则并不是只声明required就行的,正确姿势:

public function rules(){
	return [
		['username', 'required'],
		['password', 'required'],
		
		['username', 'string', 'length' => [2, 10], 'tooShort' => '用户名至少2个字符啊亲', 'tooLong' => '用户名最多10个字符呀亲!'],
		
		['password', 'string', 'length' => [6, 16], 'tooShort' => '密码由6到16个字符组成', 'tooLong' => '密码由6到16个字符组成'],
	];
}

以上rules方法就多了2个规则了,相信根据那些字面含义也猜得到用意是啥,我就不详解了

这样如果传入的username或password不是string类型就会validate失败

就算是string也要满足length的限制,否则就会根据情况返回tooShort或tooLong的错误消息

说白了就是把我们平时写的if判断换成了一定的规则来表达,免除写代码的麻烦,如果你愿意,自己在login方法里慢慢写校验代码都行,所以这个校验规则你可用可不用

然而Yii2表单模型的一大亮点恰恰就是它有校验规则,如果不用这些规则,你自己怎么另外实现逻辑层基本都没问题,个人觉得Yii2的还是挺好用的

关于rules方法的数组都能定义哪些校验规则,如何描述定义,下一篇验证器专门讲


快速填充数据

上面的例子中我new了一个Form后,进行了类似这样的代码赋值:

$form->username = 'abc';
$form->password = 'abc123';

这是为了将数据传给模型,然后模型才能把数据拿去校验,如果模型并没有得到数据,则根本没有可以校验的东西,所以有校验规则都是没用的

但你想哦,有些表单会有很多要填的东西,那后端不就要写很多赋值代码?其实有快速赋值的方法:

$_POST = [
	'username' => 'abc',
	'password' => 'abc123',
];
$form->load($_POST, '');

//输出
echo $form->username; // abc
echo $form->password; // abc123

很容易懂吧,用load方法就能把一个数组快速赋值到模型属性里了,前提是数组的key跟模型的属性命名是一模一样的才可以。

在这种用法下顺带一提,load方法的第2个参数必须是空字符串,如果按照官方权威指南所说的用ActiveForm来实现前端的话才不需要传第2个参数,这个你可以暂时不必关心

变通点,实际上咱们的代码应该这样写:

if(!$form->load(Yii::$app->request->post(), '')){
	return '接收参数失败';
}

if(!$form->validate()){
	return $form->firstError[0]; //校验失败的错误消息
}

if($form->login()){
	return '登录成功';
}else{
	return '登录失败';
}

可以多个属性共享一条规则

上面例子中,为了声明username和password是required的就定义了2条规则,其实可以这样挤在同一条规则里

本来规则的第一个值是字符串,描述一个自身的public属性嘛,多属性共享规则时,它就要变成数组了:

public function rules(){
	return [
		[['username', 'password'], 'required'], //第一个数组元素是重点,变成数组了
		
		['username', 'string', 'length' => [2, 10], 'tooShort' => '用户名至少2个字符啊亲', 'tooLong' => '用户名最多10个字符呀亲!'],
		
		['password', 'string', 'length' => [6, 16], 'tooShort' => '密码由6到16个字符组成', 'tooLong' => '密码由6到16个字符组成'],
	];
}

这样第1条规则就是两个属性共享了,都说是required的

只是string特征的校验规则里由于大家的长度和报错消息不同,所以只能单独各自定义


完整的基础示例

class LoginForm extends \yii\base\Model{

	public $username = '';
	
	public $password = '';
	
	public function rules(){
		return [
			[['username', 'password'], 'required'],
			
			['username', 'string', 'length' => [2, 10], 'tooShort' => '用户名至少2个字符啊亲', 'tooLong' => '用户名最多10个字符呀亲!'],
			
			['password', 'string', 'length' => [6, 16], 'tooShort' => '密码由6到16个字符组成', 'tooLong' => '密码由6到16个字符组成'],
		];
	}
	
	public function login(){
		if(!$this->validate()){
			//不需要addError,因为validate里面会自动addError
			return false;
		}
		
		$user = \app\model\User::findOne(['username' => $this->username]);
		if(!$user){
			$this->addError('username', '无效的用户名或密码');
			return false;
		}
		if($user->password != md5($password)){
			$this->addError('username', '无效的用户名或密码');
			return false;
		}
		
		return Yii::$app->user->login($user);
	}
}

控制器代码(和开始的一样,只是模型加了校验规则,login里执行了validate):

public function actionLogin(){
	$form = new LoginForm();
	$form->username = Yii::$app->request->post('username');
	$form->username = Yii::$app->request->post('password');

	if($form->login()){
		$errorMessage = $form->firstError[0];
		//成功的结果响应
	}else{
		//失败的结果响应
	}
}

表单模型就是前端表单的后端版本体现

为什么叫表单模型呢?其实细心点想想,前端要做登录通常就会有一个表单(form)

表单里又有username和password两个输入项,就对应表单模型的那两个属性

用户输入后,提交时,前端又会用JS代码先做一些基本的输入格式校验,不通过就提示用户;这里后端表单模型对应的就是用校验规则进行格式校验,不通过也返回错误消息

如果通过了才可以执行login业务的实体逻辑

所以你只要学会“把表单模型想像成前端表单”就很容易掌握这个东西


精华:这是一种业务模型,模型可以跟数据库无关

  • 提示:理解我下面说的意思,你就抛离大部分菜鸟程序员一段距离了

网上已经很多老程序员都发文表示模型与数据库无关,但很多新手还是停在“模型就是操作数据库的工具”这个理解阶段

前面我已经发表过Yii2的AR模型文章,那是一个便于操作数据库的模型,称为AR模型

这里是一个仅仅用于处理业务的模型,称为表单模型

其实模型是分成多个不同级别的

AR层的模型继承了yii\db\BaseActiveRecord,而yii\db\BaseActiveRecord其实又是继承了基础模型yii\base\Model

于是其实AR模型也能定义rules,也能validate,那主要是对数据库字段做校验的,可以翻一下官方文章和demo看看


模型模型,就是一个模型:比如表单模型,它是一个业务或表单的体现方式;再比如AR模型,它是数据库表里面某一条记录的体现方式

你想像一下,有个月饼模,它就是一个模型,把月饼材料放上去,一盖就出一个月饼,一个月饼就是一个模型实例,没有new的时候,它就只是一个模

可以有很多个不同的登录业务,所以能new很多个不同的LoginForm;可以有很多条不同的记录,于是也能findOne出很多个AR模型实例……

以我的能力也只能这么解释了,希望大家能明白