不败君

前端萌新&初级后端攻城狮

ThinkPHP6全局事务(中间件及订阅事件)

ThinkPHP6全局事务(中间件及订阅事件)

2020-06-19 18:12:00

围观(1046)

只要进行了多表操作, 都应该开启事务, 如果每次遇到需要多表操作的方法都手动开事务是很累的...

所以本文使用 TP6 的 中间件 + 订阅事件 + 模型事件 进行全局的事务.


安装

首先安装 TP6, 文档地址: https://www.kancloud.cn/manual/thinkphp6_0/1037481 直接看文档就能安装.


安装好了 TP6 可以玩玩 TP6 的多应用模式. 文档: https://www.kancloud.cn/manual/thinkphp6_0/1297876


使用 PHPStudy 配置好之后, 打开域名可以看到这样的页面:

1.png


控制器及多应用

在 app 下新建一个 api 目录, 当然这里并非只能创建 api 目录, 查看文档就知道是可以随意添加.

2.png

根据官方文档, 需要在 api 目录下新增几个目录: controller / model / view / config / route, 由于模型可能被多个应用共用, 所以可以不创建 model 而是在 app 目录下创建 model 目录.

3.png

在 api 目录下继续新建一个 base 用于存放自己设置的控制器基类, 可以将之前保留的 BaseController.php 复制到 api/base 下.

4.png

使用编辑器打开 BaseController.php 需要将里面的命名空间修改:

namespace app\api\base;


中间件

接着就是在这个控制器基类里面定义中间件, 所以继续在 api 新建一个目录: middleware

使用命令生成一个中间件:

php think make:middleware BaseResponse

此时这个中间件并没有在想要的地方(TP6 文档没写怎么生成在指定位置 有空博主再去看看), 可以手动移动过去.

移动过去后也一样要修改命名空间:

namespace app\api\middleware;

中间件的 handle 方法需要写入:

public function handle($request, Closure $next)
{
    $response = $next($request);
    $this->code = $response->getCode();
    return $response;
}

中间件类需要添加一个属性:

protected $code = 0;

再回到控制器, 可以直接在控制器的 $middleware 里面写上刚创建好的中间件:

protected $middleware = [BaseResponse::class];

如果用的不是 PHPStorm 需要手动 use 一下中间件. 否则会报错类不存在.


事件订阅

事件订阅的官方文档: https://www.kancloud.cn/manual/thinkphp6_0/1037492

使用命令生成事件:

php think make:event DBSubmit

打开 app / event 的 DBSubmit.php

写入代码:

private $start = false;

public function onTaskStart()
{
    if (!$this->start) {
        $this->start = true;
        Db::startTrans();
    }
}

public function onTaskSuccess()
{
    if ($this->start) {
        Db::commit();
    }
}

public function onTaskFail()
{
    if ($this->start) {
        Db::rollback();
    }
}

public function subscribe(Event $event)
{
    $event->listen('dbStartTask', [$this,'onTaskStart']);
    $event->listen('dbCommit', [$this,'onTaskSuccess']);
    $event->listen('dbRollback',[$this,'onTaskFail']);
}

需要 use 的:

use think\Event;
use think\facade\Db;

再到 app 下的 event.php 定义一下事件订阅:

'subscribe'    =>    [
    'app\event\DBSubmit',
    // 更多事件订阅
],

回到中间件, 在 BaseResponse 中间件中添加一个 end 方法并写入监听:

public function end(\think\Response $response)
{
    if ($this->code == 200) {
        // 提交数据
        event('dbCommit');
    } else {
        // 回滚数据
        event('dbRollback');
    }
}

可以简单理解为, 中间件响应成功就是代码没抛异常(后续业务逻辑判断有问题时需要手动抛异常), 就可以提交数据.


模型

在 api / base 下创建一个 BaseModel.php 并写入:

<?php
namespace app\api\base;

use think\Model;

class BaseModel extends Model
{
    
}

后面新建的模型都继承这个模型基类, 如果需要全局软删除就可以直接改写这个基类模型.

由于现在我们的目的是事务, 所以需要在这个基类模型中添加一些模型事件.

官方文档: https://www.kancloud.cn/manual/thinkphp6_0/1037598

主要就是在五个事件中添加一下开启事务.

新增前 onBeforeInsert, 更新前 onBeforeUpdate, 写入前 onBeforeWrite, 删除前 onBeforeDelete, 恢复前 onBeforeRestore.

写好之后的代码是这样的:

<?php
namespace app\api\base;

use think\Model;

class BaseModel extends Model
{
    public static function onBeforeWrite(Model $model)
    {
        self::targetDbTaskStart();
    }

    public static function onBeforeUpdate(Model $model)
    {
        self::targetDbTaskStart();
        $data = array_merge($model->getData(), [$model->updateTime => date('Y-m-d H:i:s')]);
        $model->data($data);
    }

    public static function onBeforeInsert(Model $model)
    {
        self::targetDbTaskStart();
        $data = array_merge($model->getData(), [$model->createTime => date('Y-m-d H:i:s')]);
        $model->data($data);
    }

    public static function onBeforeRestore(Model $model)
    {
        self::targetDbTaskStart();
    }

    public static function onBeforeDelete(Model $model)
    {
        self::targetDbTaskStart();
    }

    private static function targetDbTaskStart()
    {
        event('dbStartTask');
    }
}


抛出异常

最后需要在抛出异常的类里面添加回滚 (app / ExceptionHandle.php).

public function render($request, Throwable $e): Response
{
    event('dbRollback');
    // 添加自定义异常处理机制

    // 其他错误交给系统处理
    return parent::render($request, $e);
}


测试

需要先配置好数据库, TP6 和 Laravel 一样, 创建一个 .env 文件在根目录, 然后添加配置:

[database]
hostname = '127.0.0.1';
database = 'test';
username = 'root';
password = 'root';
hostport = '3306';

在本地这个数据库 test 添加一个 test 表. 并创建这个表的模型. 别忘了要继承 BaseModel.

表结构:

5.png

在 api / controller 里面创建一个 Index.php 类, 记住要继承 BaseController 类, 否则中间件不起效.

在 Index 控制器写入 test 方法:

public function test()
{
    $model_test = new Test();
    $model_test->save([
        'name' => 1
    ]);

    return $x;
}

因为变量 x 不存在, 所以这里是会报错的. 报错之后刷新数据库发现数据没有增加.


手动抛异常

因为这个中间件的原因, 如果在控制器返回类似这样的数据:

return [
    'code' => 0,
    'msg' => 'msg'
]

这样也是不会回滚的, 因为正常响应了中间件也提交了数据.

在 api 下创建一个 exception 目录. 在这个目录新建 ApiException.php 类 写入代码:

<?php

namespace app\api\exception;

use think\Response;
use Throwable;

class ApiException extends \think\Exception
{
    public static function response($request, Throwable $e): Response
    {
        $msg = $e->getMessage();
        $code = $e->getCode();
        if (empty($code)) {
            $code = 999;
        }

        return json([
            'msg' => $msg,
            'code' => $code
        ]);
    }
}

写入一个助手函数(文件在 app / common.php):

use app\api\exception\ApiException;
use think\Response;

if (!function_exists('errorService')) {

    function errorService($msg): Response
    {
        throw new ApiException($msg);
    }
}

app 下的 ExceptionHandle 类的 render 方法也要增加一个判断:

public function render($request, Throwable $e): Response
{
    event('dbRollback');
    // 添加自定义异常处理机制

    if ($e instanceof ApiException) {
        return ApiException::response($request, $e);
    }

    // 其他错误交给系统处理
    return parent::render($request, $e);
}

在 Index 控制器中, 在方法里面使用 errorService('msg', int $code); 就能返回异常信息给前端. 同时会回滚数据.

public function test()
{
    $model_test = new Test();
    $model_test->save(
        [
            'name' => 1
        ]
    );
    errorService('msg', 222);
}

6.png

把 errorService 方法注释 就可以正常存入数据.

事务如果启用正常, 注释 errorService 和 不注释 errorService 请求接口. 可以看到数据库的数据变化:

7.png


注意

数据表的引擎(类型)一定不能是 MyISAM 而是应该使用 InnoDB.

这是数据库的基本知识, 就不多说了.

本文地址 : www.bubaijun.com/page.php?id=189

版权声明 : 未经允许禁止转载!

评论:我要评论
发布评论:
Copyright © 不败君 粤ICP备18102917号-1

不败君

首 页 作 品 微 语