在项目后端开发中,最常见的就是增删改查,上一篇文章我们做了对于数据查询方面的优化,这一篇我们来继续优化增删改的代码。
问题
在对数据增删改时,通常我们会在控制器中实现,例如:
public function create()
{
//获取提交的参数
$data = $this->request->post() + ['admin_id' => $this->adminId];
//验证参数
$this->validate($data, [
'name' => 'require',
'mobile' => 'require|mobile',
]);
//保存到数据库
$model = \app\model\User::create($data);
//返回
return json($model->toArray());
}
代码的逻辑很简单,快速开发时,我也是经常这样写。如果数据添加时,有较多业务代码时,可能会把业务代码都写在控制器里或者放到模型里,比如放在模型里时:
public static function myCreate(array $data)
{
//todo 加入业务逻辑代码
//最后新增到数据库
return static::create($data);
}
然后更新下控制器方法里的代码,把 User::create($data) ,改成User::myCreate($data) 。
不过这种模式在模型里的业务代码不复杂还好,如果复杂的话,就会把控制器和模型变得杂乱和臃肿,无法做到各个角色各司其职,让代码难以维护和扩展。
解决
此时我们就需要在控制器和模型层之间多加一层——服务层(Service ),不过我愿意把这一层成为逻辑层(Logic)。就是把原本写在模型或者控制器里的业务代码抽离出来,放在逻辑层里。下面动手开整。
首先我们定义一个逻辑基类 Logic,给类加上基础的增删改查功能,完整代码如下:
<?php
namespace app\library;
use think\exception\ValidateException;
use think\Model;
class Logic
{
/**
* 模型实例
*
* @var Model
*/
protected $model;
/**
* 访问权限条件
*
* @var mixed
*/
protected $access;
/**
* 访问权限错误信息
*
* @var string
*/
protected $accessMessage;
/**
* 创建 Logic 实例并绑定模型
*
* @param string $modelName 模型类名
* @return self
*/
public static function make(string $modelName)
{
$instance = new self();
$instance->withModel(new $modelName());
return $instance;
}
/**
* 绑定模型实例
*
* @param Model $model 模型实例
* @return self
*/
public function withModel(Model $model)
{
$this->model = $model;
return $this;
}
/**
* 设置访问权限条件和错误信息
*
* @param mixed $access 访问权限条件,可以是闭包或数组
* @param string $message 无权限访问的错误信息
* @return self
*/
public function withAccess(mixed $access, string $message = '无权限访问')
{
$this->access = $access;
$this->accessMessage = $message;
return $this;
}
/**
* 创建模型实例
*
* @param array $data 创建的数据
* @return Model
* @throws ValidateException
*/
public function create(array $data)
{
$class = $this->model::class;
$model = new $class();
$model->data($data, true);
// 权限检查
$this->access && $this->checkAccess($model);
$model->save();
return $model;
}
/**
* 读取模型实例
*
* @param int $id 模型ID
* @return Model
* @throws ValidateException
*/
public function read(int $id)
{
$model = $this->model->findOrFail($id);
// 权限检查
$this->access && $this->checkAccess($model);
return $model;
}
/**
* 更新模型实例
*
* @param int $id 模型ID
* @param array $data 更新的数据
* @return Model
* @throws ValidateException
*/
public function update(int $id, array $data)
{
$model = $this->read($id);
$model->save($data);
return $model;
}
/**
* 删除模型实例
*
* @param int $id 模型ID
* @return Model
* @throws ValidateException
*/
public function delete(int $id)
{
$model = $this->read($id);
$model->delete();
return $model;
}
/**
* 检查访问权限
*
* @param Model $model 模型实例
* @throws ValidateException
*/
protected function checkAccess(Model $model)
{
if (!$this->access) {
return;
}
if (is_callable($this->access)) {
$allow = call_user_func($this->access, $model);
} else if (is_array($this->access) && !empty($this->access)) {
// 字段对比
$allow = true;
foreach ($this->access as $key => $value) {
if ($model[$key] !== $value) {
$allow = false;
break;
}
}
} else {
throw new \Exception('未设置权限检测方法');
}
if (!$allow) {
throw new ValidateException($this->accessMessage);
}
}
}
在这个逻辑类里,我们定义了增删改查方法,同时具有数据权限判断的功能,只要我们对指定的数据表没有特殊的功能,不需要写业务逻辑,就可以实现对数据的管理。
接着我们在控制器上实例化这个逻辑类,添加完整的增删改查代码:
<?php
namespace app\controller;
use app\library\Logic;
use app\BaseController;
class User extends BaseController
{
/**
* 业务逻辑类实例
*
* @var Logic
*/
protected Logic $logic;
/**
* 模拟当前登录用户的ID
*
* @var int
*/
protected int $adminId = 1;
/**
* 初始化方法,在控制器初始化时调用
*/
public function initialize()
{
// 创建业务逻辑类实例,并传入 User 模型类
$this->logic = Logic::make(\app\model\User::class);
}
/**
* 创建用户
*
* @return \think\response\Json
*/
public function create()
{
// 获取请求中的数据并添加 当前登录用户的 admin_id
$data = $this->request->post() + ['admin_id' => $this->adminId];
// 通过业务逻辑类创建用户
$result = $this->logic->create($data);
// 返回创建的用户数据
return json($result->toArray());
}
/**
* 读取用户信息
*
* @param int $id 用户ID
* @return \think\response\Json
*/
public function read($id)
{
// 通过业务逻辑类读取用户信息
return json($this->logic->read($id)->toArray());
}
/**
* 更新用户信息
*
* @param int $id 用户ID
* @return \think\response\Json
*/
public function update($id)
{
// 获取 PUT 请求中的数据
$data = $this->request->put();
// 验证数据
$this->validate($data, [
'name' => 'require',
'mobile' => 'require|mobile',
]);
// 权限检测并更新用户信息(只有编辑自己添加的数据,即读取的数据的admin_id等于当钱登录用户)
$result = $this->logic->withAccess(['admin_id' => $this->adminId])->update($id, $data);
// 返回更新结果
return json($result->toArray());
}
/**
* 删除用户
*
* @param int $id 用户ID
* @return \think\response\Json
*/
public function delete($id)
{
// 权限检测,使用闭包方式验证 admin_id 是否匹配
$this->logic->withAccess(fn($model) => $model->admin_id === $this->adminId)->delete($id);
// 返回空数组表示删除成功
return json([]);
}
}
更新后的控制器里,我们把原来让控制器从直接操作模型类写入数据改成了调用逻辑类的create方法实现数据写入。另外 Logic 类也具有权限判断功能,通过 $this->withAccess 来提供权限判断的代码。可以进行字段对比也可以直接以闭包的方式写更灵活的权限判断逻辑。
在实际项目开发过程中,必定会出现各种需求,例如在新建用户时,自动生成填充一些其他数据。这些业务代码在上面的逻辑类里自然是无法实现,此时我们只需定义一个UserLogic 类,并继承 Logic 类,接着覆盖父类的create方法,把上面所需的业务代码加入其中即可。不需要再去控制器或者模型里增加业务代码,保持了两者的单一职责。
优化
增加了逻辑层后,在每个控制器里,都会存在一个或者多个逻辑类,此时我们可以利用之前的文章《利用 PHP 8 的注解特性来实现依赖注入》,让控制器自动注入所需的逻辑类,减少不必要的重复代码。再通过文章《基于ThinkPHP里模型搜索器的高效数据查询解决方案》给 Logic 类增加数据查询的功能,让逻辑类完整地实现增删改查功能。
总结
本文介绍了通过在控制器和模型之间新增"逻辑层"(Logic Layer),来解决控制器和模型定位模糊的问题。这种方式的优点有:1、保持了控制器的简洁,只负责接收请求和返回响应;2、保持了模型的单一职责,只与数据读写相关;3、将业务逻辑代码独立出来,有利于代码维护和复用;4、通过权限检查,增强了系统的安全性。