Open woodongwong opened 2 years ago
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()()()()...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(){})
(function(){})()
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中就是使用的这个方法:
array_reduce
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)
参考:
扩展阅读:
Laravel路由中间件是一个洋葱模型,http请求会从第一个中间件经过第二个中间件、第三个中间件,最后到达控制器(一般情况下),然后再从第三个中间件返回至第二个中间件,再返回至第一个中间件,整个路线像洋葱一样一层一层的。
我们将其模型和功能简单化,将每一个中间件类中的handle方法看作是一个普通的方法,然后先理解洋葱模型的左半部分。这非常简单,假如有方法f1、f2、f3,其左边的调用顺序就是f1->f2->f3,即f3(f2(f1())),我们换一种代码形态:
(示例1)
假如有N个方法,难道要
$run()()()()...N;
?这个问题可以通过以下代码结构解决:(示例2)
注:PHP7+版本,匿名方法自动调用方式(IIFE):
(function(){})()
;php7以下版本:call_user_func(function(){})
上面两段代码展示了洋葱模型的左半部分,相信你已经知道右半部分怎么实现了,没错就是这样:
(示例3)
我们再实现一个传参版的:
(示例4)
上面的例子都是展开的代码来展示原理,那我们怎么样才能实现下面这样灵活的调用方式呢?
(示例5)
比较难理解的是foreach部分,我们再分解一下,注意观察一下$stack的值的变化:
(示例6)
通过示例6的三次循环可以看出,刚好和示例4相吻合。到这里我们通过上面的几个示例可以明白Laravel路由中间件的核心实现原理。其实代码还可以使用
array_reduce
函数来代替示例5中的foreach,laravel中就是使用的这个方法:(示例7)
参考:
扩展阅读: