代码是laravel8.57里的,其它版本应该差异不会太大吧
protected function sendRequestThroughRouter($request) { $this->app->instance('request', $request); Facade::clearResolvedInstance('request'); $this->bootstrap(); return (new Pipeline($this->app)) ->send($request) ->through($this->app->shouldSkipMiddleware() ? [] : $this->middleware) ->then($this->dispatchToRouter()); }
这个是 src/Illuminate/Foundation/Http/Kernel.php
的代码片断,默认的中间件是app/Http/Kernel.php
中的
protected $middleware = [ // \App\Http\Middleware\TrustHosts::class, \App\Http\Middleware\TrustProxies::class, \Fruitcake\Cors\HandleCors::class, \App\Http\Middleware\PreventRequestsDuringMaintenance::class, \Illuminate\Foundation\Http\Middleware\ValidatePostSize::class, \App\Http\Middleware\TrimStrings::class, \Illuminate\Foundation\Http\Middleware\ConvertEmptyStringsToNull::class, ];
然后then
方法就是在执行了:
public function then(Closure $destination) { $pipeline = array_reduce( array_reverse($this->pipes()), $this->carry(), $this->prepareDestination($destination) ); return $pipeline($this->passable); }
这个得先说明下array_reduce
这个函数用法,
array_reduce(array $array, callable $callback, $initial = null): mixed
将回调函数 callback
迭代地作用到 array
数组中的每一个单元中,从而将数组简化为单一的值。$callback的函数为callback(mixed $carry, mixed $item): mixed $carray为对$array中的上一个执行$callback回调函数的结果,$item为下一个$array中的元素
也就是先对$array中的第一个元素执行$callback函数,将结果对第二元素执行$callback时的第一个参数即$carray,第二个元素为$callback的第二个参数即$item,直到每个元素执行完。由于第一次执行时是没有上一次执行的结果的,所以 可以传一个 $initial参数,即为第一次执行$callback的$carry参数。如果不传$initial的话第一次执行$callback时的$carray就为null,就需要在$callback手动判断处理。
如果还不好理解的话,文档的评论里有个人写了个等价的函数(或者说实现的代码本体?)可以帮助理解,下面的略微改了下变量名,话说没想到这个函数居然存在得这么早,以前真的是完全没用过。。。。
function array_reduce($array, $callback, $initial=null) { $carray = $initial; foreach($array as $item) { $carray = $callback($carray, $item); } return $carray; }
而框架中的实现么,$callback为
protected function carry() { return function ($stack, $pipe) { return function ($passable) use ($stack, $pipe) { try { if (is_callable($pipe)) { return $pipe($passable, $stack); } elseif (! is_object($pipe)) { [$name, $parameters] = $this->parsePipeString($pipe); $pipe = $this->getContainer()->make($name); $parameters = array_merge([$passable, $stack], $parameters); } else { $parameters = [$passable, $stack]; } $carry = method_exists($pipe, $this->method) ? $pipe->{$this->method}(...$parameters) : $pipe(...$parameters); return $this->handleCarry($carry); } catch (Throwable $e) { return $this->handleException($passable, $e); } }; }; }
$inital为
protected function prepareDestination(Closure $destination) { return function ($passable) use ($destination) { try { return $destination($passable); } catch (Throwable $e) { return $this->handleException($passable, $e); } }; }
这个粗一看闭包的层级有点儿多,不是太好看,不是很好理解,下面弄成个精简版模型来说明
class p implements ArrayAccess { public array $data = []; public function offsetExists($offset) { return $this->data[$offset]; } public function offsetGet($offset) { return $this->data[$offset]; } public function offsetSet($offset, $value) { if ($offset === null) { return $this->data[] = $value; } return $this->data[$offset] = $value; } public function offsetUnset($offset): void { unset($this->data[$offset]); } } class a { public function call(p $param, $next): void { echo 'class a now calling', PHP_EOL; print_r($param->data); $param[] = 'a'; $next($param); echo 'class a now called', PHP_EOL; print_r($param->data); } } class b { public function call(p $param, $next) { echo 'class b called ', PHP_EOL; print_r($param->data); $param[] = 'b'; return $next($param); } } class c { public function cc(p $param): void { echo 'I am initial class but latest called', PHP_EOL; print_r($param->data); $param[] = 'c'; echo PHP_EOL; } } $aa = new a(); $bb = new b(); $cc = new c(); $middlewares = [ [$aa, 'call'], [$bb, 'call'], ]; $fn = function ($forwardRes, $nextFn) { return function ( $param) use ($forwardRes, $nextFn) { return $nextFn($param, $forwardRes); }; }; $a = array_reduce(array_reverse($middlewares), $fn, [$cc, 'cc']); $a(new p());
为了方面后面的说明就把类a,b,c的三个类call方法作为中间件的执行方法,$callback中的$carry和$item用$forwardRes和$nextFn代替,使用ArrayAccess代替普通数组更方便作为参数。$fn的作用不仅是将$middlewares中的每个callable元素包装成一个闭包函数,执行把把这个闭包在作为每个callable中的$next参数,即执行$next闭包就是在执行下一个中间件;而且也把整个$middleware数组最终生成为
function ( $param) { return $nextFn($param, $forwardRes); }
这个一个闭包。
下面是执行array_reduce时情况
第一次执行$fn时
$forwardRes为c对象的cc方法的闭包,因为调用过array_reverse,所以$nextFn为b对象的call方法,生成了一个执行时就执行b对象call方法的闭包,相当于
function ( $param) { return b::call($param, $forwardRes); }
此时的$forwardRes即为 c对象的cc方法。
然后是第二次执行
第二次执行生成了$a一个执行时就执行a对象的call方法的闭包函数,相当
function ( $param) { return a::call($param, $forwardRes); }
此时的$forwardRes即为前一个生成执行b::call的闭包函数。
最后执行$a,即先执行a对象的call方法
如图此时$next即为第一次array_reduce执行时生成的闭包,
function ( $param) { return b::call($param, $forwardRes); }
然后a的call中调用$next($param)
,就是执行那个闭包
从而执行b的call方法
如此往复执行,直到执行最开始传入的callable即c对象的cc方法后层层返回
整个调用流程就相当于a调用b,b中调用c
执行输出结果为
class a now calling
Array
(
)
class b called
Array
(
[0] => a
)
I am initial class but latest called
Array
(
[0] => a
[1] => b
)
class a now called
Array
(
[0] => a
[1] => b
[2] => c
)
可以看出来整个执行过程严重依赖于$middlewares中的元素顺序,如果不调用array_reverse的话就是b调a,a再调c了,但$initial都是最后执行的,除了如a对象的call方法那种写法外,因为正常一般都是直接return $next($param)
。真正处理中间件的业务逻辑可以写在$next方法之前或者之后,但一定要执行$next方法,不然后整个执行就会断掉。
然后laravel中的要更麻烦一些,主要注册的中间件都是类名,还得通过容器实例化为对象再调用对象的中handle方法,最后执行route中的run方法什么到controller里的方法,再将结果交给response处理,也即是$initial就是处理解析route的方法dispatchToRouter
,那个$param就是request实例,最终返回的为response实例。
这里再介绍一种在go语言里gin框架实现这种层层调用中间件的原理:
class middleware { protected array $calls = []; protected $i = -1; protected int $length = 0; /** * @param mixed $calls */ public function push(mixed $calls): void { $this->calls[] = $calls; ++$this->length; } public function next($param): void { ++$this->i; for (; $this->i < $this->length; ++$this->i) { $this->calls[$this->i]($param, [$this, 'next']); } } public function nextX($param) { ++$this->i; for (; $this->i < $this->length; ++$this->i) { return $this->calls[$this->i]($param, [$this, 'next']);//或nextX } } public function abort(): void { $this->i=count($this->calls)+1; } } $m = new middleware(); $middlewares = [[$aa, 'call'], [$bb, 'call'], [$cc, 'cc']]; foreach ($middlewares as $callable) { $m->push($callable); } $m->next(new p());
这种么是只用for循环实现,好理解得多,这里还有两种next的调用方式,用方法nextX
可以完全像array_reduce那种严格的手动调用next闭包,可以有返回值。另一种是使用方法next
,这种可以不用手动调用next闭包就可以调用下一个中间件,不过不能有返回值(返回值可以通过引用类型的参数传递),而且中断的话就必须得手动调用abort方法了,这样的话可能还得调整个中间件方法的参数,略麻烦些。但两种方式注册的中间件最后一个都需同array_reduce方式的$initial的函数原型一致或者最后一个不调用$next函数。
最后么感觉用array_reduce的方式其实是要方便得多,虽然那个构造闭包的function真的是绕得想撞墙的节奏就是了。是得要最先就得确定好最终或者说那个initial闭包的原型,然后记得中间件的那个next参数一定就是那个闭包后其实还好。哦,对了这个又叫做柯里化,不得不说不管哪种方式实现这种层层调用的人真可乃神人也。。。。。
谨以此文,致曾经面试自己的那位cto面试官及对代码有追求的人,其实当时自己只会用for的那种方式 🤣 🤣 🤣。。。。。。