理解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)
参考:
- IIFE:en.wikipedia.org/wiki/Immediately_...
- PHP箭头函数:www.php.net/manual/zh/functions.ar...
- PHP array_reduce函数:www.php.net/manual/zh/function.arr...
- Laravel Pipeline:github.com/laravel/framework/tree/...
扩展阅读:
本作品采用《CC 协议》,转载必须注明作者和本文链接
原来如此
這個東西有點繞 每次去看它源碼的時候 又要繞一會才繞出來 :joy:
牛批