40. 模型数据验证
简介
本节我们在后端存储时添加上和前端视图表单相同的验证,并且对登录密码进行加密存储。
需求分解
虽然我们上节给视图表单添加了数据验证,但从开发角度来讲后端无论什么情况下也不要 完全信任 前端表单提交的数据,所以后端 PHP 程序也要对数据进行相应的验证。
在后端我们将使用 验证器 完成对表单数据所验证。只有当所有字段验证规则通过时才允许保存到数据库,否则抛出错误异常。
参照后台模块管理员( Adminer )登录密码加密方式,我们也是用数据模型的 [修改器] 功能和 password_hash
函数实现登录密码的加密保存。
验证异常
表单数据验证失败我们可以理解为是用户提交表单的数据不合法,这些属于项目业务逻辑管理范畴,通常我们在项目开发中对于业务逻辑不合法的操作都是让方法抛出异常,而在展示层我们会捕获异常然后给用户展示一些友好的提示信息,并不是直接给用户显示程序执行异常错误。所以我们先创建一个专门处理验证异常类—— ValidateException 。
application/common/exception/ValidateException.php
<?php
namespace app\common\exception;
class ValidateException extends \Exception
{
protected $data = [];
/**
* 设置异常额外的数据
* @Author zhanghong(Laifuzi)
* @DateTime 2019-06-10
* @param array $data [description]
*/
public function setData(array $data)
{
$this->data = $data;
}
/**
* 获取异常额外Debug数据
* @Author zhanghong(Laifuzi)
* @DateTime 2019-06-10
* @return array [description]
*/
final public function getData()
{
return $this->data;
}
}
验证器
使用命令行工具创建注册用户验证器:
$ php think make:validate common/User
验证器类代码如下:
application/common/validate/User.php
namespace app\common\validate;
use think\Validate;
class User extends Validate
{
protected $rule = [
'name' => 'require|length:2,20',
'mobile' => 'require|min:13000000000|max:19900000000|unique:user',
'password' => 'require|length:6,20',
'password_confirmation' => 'require|length:6,20|confirm:password',
];
protected $message = [
'name.require' => '用户名不能为空',
'name.length' => '用户名长度必须在2-20个字符之间',
'mobile.require' => '手机号码不能为空',
'mobile.min' => '手机号码格式不正确',
'mobile.max' => '手机号码格式不正确',
'mobile.unique' => '当前手机号码已注册',
'password.require' => '登录密码不能为空',
'password.length' => '登录密码长度必须在6-20之间',
'password_confirmation.require' => '重复密码不能为空',
'password_confirmation.length' => '重复密码长度必须在6-20之间',
'password_confirmation.confirm' => '两次输入的密码不一致',
];
}
代码解读
- 通常后端程序的数据验证规则应该和视图页面验证保持一致,所以我们定义的验证规则和注册表单保持相同;
- 验证器的错误提示可以使用该类的
field
和message
两个属性定义,两者的区别是field
用于设置属性名,规则提示将使用框架默认提示语句;而message
是设置每条验证规则的完全提示信息。在本教程里我们默认使用message
来设置验证规则错误提示信息。
数据模型
接下来,我们在注册用户模型里添加一个处理注册逻辑方法,在该方法里调用上面定义的验证器来验证提交数据是否合法,同时实现登录密码加密读写器。详细代码如下:
application/common/model/User.php
<?php
namespace app\common\model;
use think\Model;
use app\common\validate\User as Validate;
use app\common\exception\ValidateException;
class User extends Model
{
.
.
.
/**
* 注册新用户
* @Author zhanghong(Laifuzi)
* @DateTime 2019-06-10
* @param array $data 表单提交数据
* @return User 新注册用户信息
*/
public static function register($data)
{
$validate = new Validate;
if(!$validate->batch(true)->check($data)){
$e = new ValidateException('注册数据验证失败');
$e->setData($validate->getError());
throw $e;
}
try{
$user = new self;
$user->allowField(true)->save($data);
}catch (\Exception $e){
throw new \Exception('创建用户失败');
}
return $user;
}
/**
* 密码保存时进行加密
* @Author zhanghong(Laifuzi)
* @DateTime 2019-06-10
* @param string $value 原始密码
*/
public function setPasswordAttr($value)
{
return password_hash($value, PASSWORD_DEFAULT);
}
}
代码解读
- 在
register
方法里我们进行数据验证时使用了验证器的batch
这个方法,batch
方法只有一个布尔类型的参数,当参数值为ture
时表示执行check
方法时当验证失败返回每个字段的第一个失败提示信息,当调用参数为false
时验证失败时只返回第一个失败字段的第一个验证失败提示信息。也就是说batch
方法决定了getError
返回的是一个数组还是字符串。 - 很多人在开发过程中喜欢使用
md5
函数加密登录密码,而我们在setPasswordAttr
方法里使用password_hash
函数是因为它比md5
函数更安全,因为md5
函数对同一字符串的加密结果值是相同的。也正因为如此,很多网站在使用md5
函数加密登录密码时通常会给数据库表多加一个字段salt
或其它的混淆字段,每条记录的salt
是一个随机字符串,然后使用md5($password.$salt)
生成加密后的密码,避免被黑客很容易破解登录密码。
控制器
最后,我们在控制器里调用数据模型方法来处理注册逻辑:
application/index/controller/Register.php
<?php
namespace app\index\controller;
use think\Request;
use app\common\model\User;
use app\common\exception\ValidateException;
class Register extends Base
{
.
.
.
public function save(Request $request)
{
$param = $request->post();
try{
$user = User::register($param);
}catch (ValidateException $e){
$this->assign('user', $param);
$this->assign('errors', $e->getData());
return $this->fetch('create');
}catch (\Exception $e){
$this->error($e->getMessage());
}
$this->success('注册成功', url('[page.root]'));
}
}
注意: User::register
有可能会抛出异常,所以不要忘记在顶部引入 ValidateException
。
效果展示
知识点
为什么我们为什么把业务代码写到模型里?
我们把业务代码写到模型而不是写在控制器里,除了遵循 重模型轻控制器原则 以外,个人觉得还有以下好处:
- 同时可以遵循 DRY 原则,因为在项目开发过程中很多时侯业务代码并不仅仅只在控制器里调用,可能在迁移文件、数据填充和自定义命令行,甚至是终端命令行模式(如 Laravel 的
artisan tinker
)调用; - 把业务代码写到模型里页面跳转流程写到控制器里方便写测试代码;
- 把业务代码写到模型里在发布或部署项目时可以对业务代码进行加密,而写在控制器里的话不能进行加密。
提交代码
下面把代码纳入到版本管理:
$ git add -A
$ git commit '给用户模型添加数据验证'
推荐文章: