PHP 框架中间件实现

7,710 阅读3分钟

0x00 前言

中间件是很多 PHP 框架都提供的功能,在初次认识它的时候我感到惊讶和兴奋。因为它的作用太强大了,在没有中间件之前我们不得不将权限验证和一些公共操作都写在控制器方法里,然后控制器就会变得很臃肿,降低了可读性和可维护性。但有了中间件我们就可以这些操作都写在中间件里,然后通过使用不同的中间件组合不仅能够实现需求还降低了代码的耦合度。既然中间件百般好,那它到底是如何实现的呢?在阅读 LaravelSlim 的源码过程中(一个让人感觉很费劲,很折磨,觉得自己很菜的受虐过程/(ㄒoㄒ)/~~),我发现其重点就是要将多个中间件闭包(有些框架中间件并不是通过闭包实现但都属于 callable 的范畴,为了行文方便统称为闭包)通过 array_reduce 或循环的方式将其打包成为一个闭包的过程。

0x01 预热

一说道中间件往往就会让人联想到这幅图

middleware

看起来很神奇很吓人的样子但是仔细观察一下,其实这个流程其实像不像函数的嵌套调用 Middleware2(Middleware1(App())) 呢?不过这样嵌套调用显然是错的,因为 PHP 先执行了 App() 而不是 Middleware2 但是如果比作嵌套的闭包呢?下面给出示例代码:

$allMiddleware = function () {
    echo 'start middleware2' . PHP_EOL;
  
    (function () {
        echo 'start middleware1' . PHP_EOL;
        // app
        (function () {
            echo 'app' . PHP_EOL;
        })();
        // end app
        echo 'end middleware1' . PHP_EOL;
    })();
  
    echo 'end middleware2' . PHP_EOL;
};
$allMiddleware();

// 输出
// start middleware2
// start middleware1
// app
// end middleware1
// end middleware2

0x02 思考

嘿,上面的代码结果和预期一样,但这代码似乎有点简(ruo)陋(zhi)。但是其实上面代码中的 $allMiddleware 就是中间件组合之后的结果,所以我们已经摸到门槛了,现在请思考如何将下列中间件闭包自动组合?

// 数据库中间件
$db = function (Closure $next) {
    echo '成功建立数据库连接' . PHP_EOL;
    $next();
    echo '成功关闭数据库连接' . PHP_EOL;
};
// 点赞中间件
$like = function (Closure $next) {
    echo '点赞+1' . PHP_EOL;
    $next();
    echo '点赞+2' . PHP_EOL;
};
// 内容闭包
$app = function () {
    echo '文章内容' . PHP_EOL;
};

多了个参数 $next 是什么鬼? $next 应该理解为「本中间件后所有中间件闭包函数打包成的一个闭包」,就以 Middleware2 而言,它的 $next 就是 Middleware1APP 打包成的闭包(参照上图的最里面两层)。

0x03 解答

现给出答案代码如下:

// array_reduce 实现
$allMiddleware = [$like, $db];
$go = array_reduce($allMiddleware, function ($next, $middleware) {
    return function () use ($next, $middleware) {
        $middleware($next);
    };
}, $app);
$go();

// foreach 实现
$allMiddleware = [$like, $db];
$next = $app;
foreach ($allMiddleware as $middleware) {
    $next = function () use ($next, $middleware) {
        return $middleware($next);
    };
}
$next();

两种实现方式但原理一样,所以只解释 foreach 版本的实现。首先是将所有的中间件组成一个数组,并将 $next 设置为 $app,然后开始循环的将 $nextmiddleware 组成一个新的闭包并赋值给 $next,这样 $next 便不断的将之前的闭包合并,最后变成一个。然后通过执行最后的 $next 即可得出结果。当然我空口白话的说可能还不好理解,最好的方式是将这代码自己在大脑执行一遍,还不行就动手画一下(我第一次看到这个代码就是这样才懂的/(ㄒoㄒ)/~~)然后就可以明白了。 此外不清楚 array_reduce 函数作用的可以 点此

0x04 好点的版本

为了好理解前面的中间件就只有 $next 参数,但实际框架的中间件都会有类似 $request 参数并且支持返回值 $response(比如 Laravel),下面是一个类似 Laravel 的中间件(仅仅是模仿,照虎画猫)实现代码。一法通,万法通,本菜逼就不解释了(万一涉及到我的知识盲区就 GG 了,O(∩_∩)O哈哈~)。

$db = function ($request, Closure $next) {
    echo '成功建立数据库连接' . PHP_EOL;
    $response = $next($request);
    echo '成功关闭数据库连接' . PHP_EOL;

    return $response;
};

$like = function ($request, Closure $next) {
    echo '点赞+1' . PHP_EOL;
    $response = $next($request);
    echo '点赞+2' . PHP_EOL;

    return $response;
};

$app = function ($request) {
    echo $request . PHP_EOL;
    return '一个无聊的返回值';
};

$allMiddleware = [$like, $db];
$next = $app;
foreach ($allMiddleware as $middleware) {
    $next = function ($request) use ($middleware, $next) {
        return $middleware($request, $next);
    };
}

$response = $next('O(∩_∩)O');
echo $response;

0x05 总结

(⊙v⊙)嗯,一点点内容就水了这么多,心满意足O(∩_∩)O哈哈~。另外文章中若出现错误,希望大家能够指出,若有疑问可以互相讨论:-D。

我的博客原文