🔥码云GVP开源项目 12k star Uniapp+ElementUI 功能强大 支持多语言、二开方便! 广告
[TOC] # 说明 ThinkPHP 6.0 对中间件的实现进行了巨大的改进,比起之前版本的实现更加简洁、有序、精妙,可以说是实现了质的飞跃,紧跟世界潮流。这篇文章对其实现细节进行分析。 整个实现过程初看起来有点复杂,但只要掌握了其前后的递推关系,理解起来就可以豁然开朗。 接着上一篇,我们分析了`runWithRequest`方法的前两行代码,接着分析它后面的代码: ``` protected function runWithRequest(Request $request) { . . . return $this->app->middleware->pipeline() ->send($request) ->then(function ($request) { return $this->dispatchToRoute($request); }); } ``` 中间件的执行都在最后的`return`语句中。 # pipeline、through、send方法 `$this->app->middleware->pipeline()`的` pipeline`方法: ``` public function pipeline(string $type = 'global') { return (new Pipeline()) // array_map将所有中间件转换成闭包,闭包的特点: // 1. 传入参数:$request,请求实例; $next,一个闭包 // 2. 返回一个Response实例 ->through(array_map(function ($middleware) { return function ($request, $next) use ($middleware) { list($call, $param) = $middleware; if (is_array($call) && is_string($call[0])) { $call = [$this->app->make($call[0]), $call[1]]; } // 该语句执行中间件类实例的handle方法,传入的参数是外部传进来的$request和$next // 还有一个$param是中间件接收的参数 $response = call_user_func($call, $request, $next, $param); if (!$response instanceof Response) { throw new LogicException('The middleware must return Response instance'); } return $response; }; // 将中间件排序 }, $this->sortMiddleware($this->queue[$type] ?? []))) ->whenException([$this, 'handleException']); } ``` `through`方法代码: ``` public function through($pipes) { $this->pipes = is_array($pipes) ? $pipes : func_get_args(); return $this; } ``` 前面调用 `through` 传入的 `array_map(...) `把所有中间件逐个封装为闭包,然后,`through`则是把这些闭包保存在Pipeline类的 `$pipes` 属性中。 PHP的 `array_map` 方法签名: ``` array_map ( callable $callback , array $array1 [, array $... ] ) : array ``` $callback迭代作用于每一个 $array的元素,返回新的值。所以,最后得到`$pipes`中每个闭包的形式特征是这样的(伪代码): ``` function ($request, $next) { $response = handle($request, $next, $param); return $response; } ``` 该闭包接收两个参数,一个是请求实例,一个是回调用函数,handle方法处理后得到相应并返回。 记住这个结构,对后面的分析非常重要。 `through` 返回一个Pipeline类的实例,接着调用`send`方法: ``` public function send($passable) { $this->passable = $passable; return $this; } ``` 该方法很简单,只是将传入的请求实例保存在`$passable`成员变量,最后同样返回`Pipeline`类的实例,这样就可以链式调用Pipeline类的其他方法。 # then,carry方法 `send`方法之后,接着调用`then`方法: ``` return $this->app->middleware->pipeline() ->send($request) ->then(function ($request) { return $this->dispatchToRoute($request); }); ``` 这里的`then`接收一个闭包作为参数,这个闭包实际上包含了控制器操作的执行代码。 `then`方法代码: ``` public function then(Closure $destination) { $pipeline = array_reduce( //用于迭代的数组(中间件闭包),这里将其倒序 array_reverse($this->pipes), // array_reduce需要的回调函数 $this->carry(), //这里是迭代的初始值 function ($passable) use ($destination) { try { return $destination($passable); } catch (Throwable | Exception $e) { return $this->handleException($passable, $e); } }); return $pipeline($this->passable); } ``` `carry`代码: ``` protected function carry() { // 1. $stack 上次迭代得到的值,如果是第一次迭代,其值是后面的「初始值 // 2. $pipe 本次迭代的值 return function ($stack, $pipe) { return function ($passable) use ($stack, $pipe) { try { return $pipe($passable, $stack); } catch (Throwable | Exception $e) { return $this->handleException($passable, $e); } }; }; } ``` 为了更方便分析原理,我们把`carry`方法内联到`then`中去,并去掉错误捕获的代码,得到: ``` public function then(Closure $destination) { $pipeline = array_reduce( array_reverse($this->pipes), function ($stack, $pipe) { return function ($passable) use ($stack, $pipe) { return $pipe($passable, $stack); }; }, function ($passable) use ($destination) { return $destination($passable); }); return $pipeline($this->passable); } ``` 这里关键是理解`array_reduce`以及`$pipeline($this->passable)`的执行过程,这两个过程可以类比于「包洋葱」和「剥洋葱」的过程。 `array_reduce`第一次迭代,`$stack`初始值为: **(A)** ``` function ($passable) use ($destination) { return $destination($passable); }); ``` 回调函数的返回值为: **(B)** ``` function ($passable) use ($stack, $pipe) { return $pipe($passable, $stack); }; ``` 将A代入B可以得到第一次迭代之后的`$stack`的值: **(C)** ``` function ($passable) use ($stack, $pipe) { return $pipe($passable, function ($passable) use ($destination) { return $destination($passable); }) ); }; ``` 第二次迭代,同理,将C代入B可得: **(D)「洋葱」** ``` // 伪代码 // 每一层的$pipe都代表一个中间件闭包 function ($passable) use ($stack, $pipe) { return $pipe($passable, //倒数第二层中间件 function ($passable) use ($stack, $pipe) { return $pipe($passable, //倒数第一层中间件 function ($passable) use ($destination) { return $destination($passable); //包含控制器操作的闭包 }) ); }; ); }; ``` 以此类推,有多少个中间件,就代入多少次,最后一次得到`$stack`就返回给`$pipeline`。由于前面对中间件闭包进行了倒序,排在前面的闭包被包裹在更里层,所以倒序后的闭包越是后面的在外面,从正序来看,则变成越前面的中间件在最外层。 层层包裹好闭包后,我们得到了一个类似洋葱结构的「超级」闭包**D**,该闭包的结构如上面的代码注释所示。最后把`$request`对象传给这个闭包,执行它:`$pipeline($this->passable);`,由此开启一个类似剥洋葱的过程,接下来我们看看这洋葱是怎么剥开的。 # 剥洋葱过程分析 回顾上文,`array_map(...)`把每一个中间件类加工成一个类似这种结构的闭包: ``` function ($request, $next) { $response = handle($request, $next, $param); return $response; } ``` 其中`handle`是中间件中的入口,其结构特点是这样的: ``` public function handle($request, $next, $param) { // do sth ------ M1-1 / M2-1 $response = $next($request); // do sth ------ M1-2 / M2-2 return $response; } ``` 我们上面的 D「洋葱」一共只有两层,也就是有两层中间件的闭包,假设M1-1,M1-2分别是第一个中间件handle方法的前置和后置操作点位,第二个中间件同理,前置和后置点位分别是M2-1,M2-2。现在,让程序执行`$pipeline($this->passable)`,展开来看,也就是执行: ``` // 伪代码 function ($passable) use ($stack, $pipe) { return $pipe($passable, function ($passable) use ($stack, $pipe) { return $pipe($passable, function ($passable) use ($destination) { return $destination($passable); }) ); }; ); }($this->passable) ``` 此时,程序要求从: ``` return $pipe($passable, function ($passable) use ($stack, $pipe) { return $pipe($passable, function ($passable) use ($destination) { return $destination($passable); }) ); }; ); ``` 返回值,也就是要执行第一个中间件闭包,`$passable`对应`handle`方法的`$request`参数,而下一层闭包 ``` function ($passable) use ($stack, $pipe) { return $pipe($passable, function ($passable) use ($destination) { return $destination($passable); }) ); } ``` 则对应`handle`方法的`$next`参数。 示意图: ![](https://img.kancloud.cn/66/cc/66cc0be2e30a98ef35ce6282b4cd2bfa_708x268.png) 要执行第一个闭包,即要执行第一个闭包的`handle`方法,其过程是:首先执行**M1-1**点位的代码,即前置操作,然后执行`$response = $next($request);`,这时程序进入执行下一个闭包,`$next($request)`展开来,也就是: ``` function ($passable) use ($stack, $pipe) { return $pipe($passable, function ($passable) use ($destination) { return $destination($passable); }) ); }($request) ``` 依此类推,执行该闭包,即执行第二个中间件的`handle`方法,此时,先执行**M2-1**点位,然后执行`$response = $next($request)`,此时的`$next`闭包是: ``` function ($passable) use ($destination) { return $destination($passable); }) ``` 属于洋葱之芯——最里面的一层,也就是包含控制器操作的闭包,执行这个闭包: ``` function ($passable) use ($destination) { return $destination($passable); })($request) ``` 最终,我们从`return $destination($passable)`中返回一个`Response`类的实例(具体怎么返回的,后面再作分析),也就是,第二层的`$response = $next($request)`语句成功得到了结果,接着执行下面的语句,也就是**M2-2**点位,最后第二层闭包返回结果,也就是第一层闭包的`$response = $next($request)`语句成功得到了结果,然后执行这一层闭包该语句后面的语句,即**M1-2**点位,该点位之后,第一层闭包也成功返回结果,于是,then方法最终得到了返回结果。 整个过程过来,程序经过的点位顺序是这样的:**M1-1→M2-1→控制器操作→M2-2→M1-2→返回结果**。 # 总结 整个过程看起来虽然复杂,但不管中间件有多少层,只要理解了前后两层中间件的这种递推关系,洋葱是怎么一层层剥开又一层层返回的,来多少层都不在话下。