理解Laravel中间件核心实现原理

Laravel路由中间件是一个洋葱模型,http请求会从第一个中间件经过第二个中间件、第三个中间件,最后到达控制器(一般情况下),然后再从第三个中间件返回至第二个中间件,再返回至第一个中间件,整个路线像洋葱一样一层一层的。

图片

我们将其模型和功能简单化,将每一个中间件类中的handle方法看作是一个普通的方法,然后先理解洋葱模型的左半部分。这非常简单,假如有方法f1、f2、f3,其左边的调用顺序就是f1->f2->f3,即f3(f2(f1())),我们换一种代码形态:

$run = function() {   // 将匿名方法赋值给变量run,当执行 $run() 时,相当于调用了这一层方法,返回的是f1方法
    return function () {  // 相当于f1,当执行 $run()() 时,相当于调用了这一层方法,输出f1,返回的是f2方法
        echo 'f1';
        return function () {// 相当于f2,当执行 $run()()() 时,相当于调用了这一层方法,输出f2,返回的是f3方法
            echo 'f2';
            return function () { // 相当于f3,当执行 $run()()()() 时,相当于调用了这一层方法,输出f3,没有返回值
                echo 'f3';
            };
        };
    };
};

$run()()()(); // 输出:f1 f2 f3

(示例1)

假如有N个方法,难道要$run()()()()...N;?这个问题可以通过以下代码结构解决:

$run = function() {              // 将匿名方法赋值给变量run
    return (function() {         // 相当于f1,自动调用,输出f1并返回调用结果
        echo 'f1';
        return (function() {     // 相当于f2,自动调用,输出f2并返回调用结果
            echo 'f2';
            return (function() { // 相当于f3,自动调用,输出f3,没有返回值
                echo 'f3';
            })();
        })();
    })();
};

$run(); // 输出:f1 f2 f3

(示例2)

注:PHP7+版本,匿名方法自动调用方式(IIFE):(function(){})();php7以下版本:call_user_func(function(){})

上面两段代码展示了洋葱模型的左半部分,相信你已经知道右半部分怎么实现了,没错就是这样:

$run = function() {
    return (function() {
        echo 'f1-left';
        $result = (function() {
            echo 'f2-left';
            $result = (function() {
                echo 'f3';
            })();

            echo 'f2-right';
            return $result;
        })();

        echo 'f1-right';
        return $result;
    })();
};

$run(); // 输出:f1-left f2-left f3 f2-right f1-right

(示例3)

我们再实现一个传参版的:

$run = function(array $arr) {
    return (function(array $arr) {
        $arr[] = 'f1-left';
        $arr = (function(array $arr) {
            $arr[] = 'f2-left';
            $arr = (function(array $arr) {
                $arr[] = 'f3';
                return $arr;
            })($arr);

            $arr[] = 'f2-right';
            return $arr;
        })($arr);

        $arr[] = 'f1-right';
        return $arr;
    })($arr);
};

$arr = $run(['start']);
$arr[] = 'end';
print_r($arr); // 输出:
/*
Array
(
   [0] => start
   [1] => f1-left
   [2] => f2-left
   [3] => f3
   [4] => f2-right
   [5] => f1-right
   [6] => end
)
*/

(示例4)

上面的例子都是展开的代码来展示原理,那我们怎么样才能实现下面这样灵活的调用方式呢?

$arr = (new Pipeline(['f1', 'f2', 'f3', ...]))->run(['start']);
$arr[] = 'end';
// 定一个 Pipeline 类
class Pipeline
{
    // 存放f1 f2 f3...N的方法名
    protected $pipes = [];

    public function __construct(array $pipes)
    {
        $this->pipes = $pipes;
    }

    public function run($data)
    {
        // 需定义一个“芯”,这样就f1 f2 f3都可以使用统一的参数,就不需要指定f3作为最中间的芯了
        $stack = function($data) {
            return $data;
        };

        // php7.4可以这样写: $stack = fn($data) => $data;

        // f1 f2 f3 变成 f3 f2 f1,因为需要先从“洋葱芯”开始包装
        $pipes = array_reverse($this->pipes);

        // 循环包装每一个方法
        foreach ($pipes as $pipe) {
            // 每次循环,$stack的层级都会增加
            $stack = function ($data) use($pipe, $stack) {
                return $pipe($data, $stack);
            };
        }

        // 相当于上面例子中的 $run();
        return $stack($data);
    }
}

/**
 * @param array    $arr
 * @param \Closure $next 匿名方法
 *
 * @return mixed
 */
function f1(array $arr, \Closure $next)
{
    $arr[] = 'f1-left';
    $arr = $next($arr);
    $arr[] = 'f1-right';
    return $arr;
}

/**
 * @param array    $arr
 * @param \Closure $next 匿名方法
 *
 * @return mixed
 */
function f2(array $arr, \Closure $next)
{
    $arr[] = 'f2-left';
    $arr = $next($arr);
    $arr[] = 'f2-right';
    return $arr;
}

/**
 * @param array    $arr
 * @param \Closure $next 匿名方法,这里比上面例子中的f3多了一个参数,因为Pipeline::run中定义了一个“芯”
 *
 * @return mixed
 */
function f3(array $arr, \Closure $next)
{
    $arr[] = 'f3';
    $arr = $next($arr);
    return $arr;
}

$arr = (new Pipeline(['f1', 'f2', 'f3']))->run(['start']);
$arr[] = 'end';
print_r($arr);  // 输出:
/*
Array
(
    [0] => start
    [1] => f1-left
    [2] => f2-left
    [3] => f3
    [4] => f2-right
    [5] => f1-right
    [6] => end
)
*/

(示例5)

比较难理解的是foreach部分,我们再分解一下,注意观察一下$stack的值的变化:

// 第0次循环
$stack = function($data) {
    return $data;
};

// 循环每一个方法
$pipes = ['f3', 'f2', 'f1'];
foreach ($pipes as $pipe) {
    // 每次循环,$stack的层级都会增加
    $stack = function ($data) use($pipe, $stack) {
        return $pipe($data, $stack);
    };

    // 第1次循环
    $stack = function($data) {
        return f3($data, function($data) {
            return $data;
        });
    };

    // 第2次循环
    $stack = function($data) {
        return f2($data, function ($data) {
            return f3($data, function($data) {
                return $data;
            });
        });
    };

    // 第3次循环
    $stack = function($data) {
        return f1($data, function ($data) {
            return f2($data, function ($data) {
                return f3($data, function($data) {
                    return $data;
                });
            });
        });
    };
}

(示例6)

通过示例6的三次循环可以看出,刚好和示例4相吻合。到这里我们通过上面的几个示例可以明白Laravel路由中间件的核心实现原理。其实代码还可以使用array_reduce函数来代替示例5中的foreach,laravel中就是使用的这个方法

public function run($data)
{
    return array_reduce(array_reverse($this->pipes), function ($stack, $item) {
        return function ($data) use($item, $stack) {
            return $item($data, $stack);
        };
    }, fn($data) => $data)($data);
}

(示例7)

参考:

扩展阅读:

原文:github.com/woodongwong/notes/issue...

php
本作品采用《CC 协议》,转载必须注明作者和本文链接
《L03 构架 API 服务器》
你将学到如 RESTFul 设计风格、PostMan 的使用、OAuth 流程,JWT 概念及使用 和 API 开发相关的进阶知识。
《G01 Go 实战入门》
从零开始带你一步步开发一个 Go 博客项目,让你在最短的时间内学会使用 Go 进行编码。项目结构很大程度上参考了 Laravel。
讨论数量: 2

原来如此

2天前 评论

這個東西有點繞 每次去看它源碼的時候 又要繞一會才繞出來 :joy:

21小时前 评论

讨论应以学习和精进为目的。请勿发布不友善或者负能量的内容,与人为善,比聪明更重要!