故事的开始
先不着急着分析源码 ps: 前戏得足 我们需要模拟出一个场景带入。假设我们现在需要计算这样的一个值,后面的结果都依赖于前面计算的值,现在我就是要通过中间件去实现这个功能。
为了减少计算量,这里我们主要看 E 这个计算公式部分,我们把整个 E 也看成 4 个部分,1 + 增值税税率 (即 count1),1/count1 (即 count2),count2 * 核定应税所得率 (即 count3) 以及 count3 * 倒算适用表税率 (即 count4).
场景出来了,我们创建四个类,分别代表 4 个中间件,count1……, 这里我们再强行制造一个场景,我们在计算 count3 的 时候,需要依赖于 count4 的计算结果,但是由于某些不可描述的约束,count3 又必须在 count4 之前进行一些条件的验证,也就是说 count3 的中间件走在 count4 的前面,但是计算却要依赖 count4 的结果处理。然后我们创建了四个类,把上面的四个步骤计算分别放入到这四个类中,我们也和平时使用中间件一样,增加一个 handle() 方法。
class Count1 { public function handle($value, \Closure $next) { $value += 1; var_dump('count1计算出来的值:' . $value); if ($value < 0) { return "系统异常"; } return $next($value); } } class Count2 { public function handle($value, \Closure $next) { $value = 1 / $value; var_dump("count2 计算出来的值:$value") . PHP_EOL; return $next($value); } } class Count3 { //核定应税所得率依赖于后面的结果 public function handle($value, \Closure $next) { $res = $next($value); var_dump("核定应税所得率收到的结果:$res") . PHP_EOL; return $res* 0.5; } } class Count4 { //倒算税率值0.5 public function handle($value, \Closure $next) { $value *= 0.5; var_dump("倒算税率值计算后的值:$value") . PHP_EOL; return $next($value); } }
Pipeline 组件
Laravel 中间件处理的核心就是 Illuminate\Pipeline 这个组件类,在了解这个类之前,我们先看看中间件在 Larave 中扮演的角色。
在到达 Application 之前的中间件是前置中间件,比如一些验证 csrf, 判断是否登录,是否有权限。在 Application 之后的是后置中间件,就是格外的处理一些逻辑,比如给响应添加请求头,cookie……。
你看上面的类格式是不是很像你们平常写的中间件代码。唯一不同的是,我们现在不走路由或者其他方式走中间件,而是直接把底层处理中间件的 Pipeline 类 (一般说管道) 拿出来,执行代码。我们运行实验类的 test 方法
public function test() { $pipes = [(new Count1), (new Count2), (new Count3), (new Count4)]; $this->dispatcher(1, $pipes); } //count4 算完之后的处理 处理结果返回到count3 public function additionalHandle() { return function ($value) { return $value < 0.1 ? $value : $value * 0.5; }; } public function dispatcher($value, $pipes) { $result = (new Pipeline)->send($value)->through($pipes)->then($this->additionalHandle()); echo "最终结果: " . $result; }
这个流程中,先实例化了 Pipeline 类,调用 send 方法,传入我们初始化的值,也就是 1,赋值给变量 passable, 返回本身,继续调用 through, 定义的四个中间件类赋值给变量 pipes,然后继续调用 then,该方法需要传入一个闭包函数。这个闭包函数就是上面的 additionalHandle 函数返回的闭包。我们再来看结果,先经过前置中间件 count 1,2,4, 到达另类的 Application,到达这里后,我们开始响应了,把结果返回给后置中间键 count3 进行计算,count3 前面已经没有其他的后置处理了,最终由 count3 返回最终的结果。最终结果:
有注意到在 count1 中的一个判断吗
我们现在传入 -2 ,然后打印结果
这样的话,不满足条件,就再也进不了下一层 (前置) 中间件了。
我们可以开始分析源码,试着去找出 Laravel 是如何优雅的实现这个流程的。直接查看
then public function then(Closure $destination) { //$destination 就是我们传入的additionalHandle返回的闭包 // return function ($value) { // return $value < 0.1 ? $value : $value * 0.5; // }; // $this->>pipes 就是传入中间件count1 2 3 4 $pipeline = array_reduce( array_reverse($this->pipes), $this->carry(), $this->prepareDestination($destination) ); //$this->passable 就是$value值,初始的时候传入的是1 return $pipeline($this->passable); }
处理的核心就是在这行代码里面了,在继续分析代码之前,我们有必要了解一下 array_reduce 这个函数。可以说,Laravel 中的中间件完全依赖于这个函数。
array-reduce 函数
本来想想还是直接用一句:这个函数具体使用去查看文档,问了就是查文档。但是因为这个函数的重要性,还是写个没有 变异的案例吧。
假如我们需要把数组里的数求和
$data = [1, 2, 3, 4]; $res = array_reduce($data, function ($carry, $item) { return $carry + $item; }); var_dump($res).PHP_EOL; //有初始值5,那么传入第三个参数 $res = array_reduce($data, function ($carry, $item) { return $carry + $item; },5); var_dump($res);
够简单了吧。稍微了解一下 array_reduce 函数我们可以往下走了。我们重新回到之前的这段代码:
$pipeline = array_reduce( array_reverse($this->pipes), $this->carry(), $this->prepareDestination($destination) );
现在你已经知道三个参数在函数中的含义了。 array_reverse 会将我们定义的四个中间件全部通过 carry 函数处理 。我们来看看 carry 方法里面是啥。
/** * Get a Closure that represents a slice of the application onion. * * @return \Closure */ protected function carry() { return function ($stack, $pipe) { return function ($passable) use ($stack, $pipe) { if (is_callable($pipe)) { // If the pipe is an instance of a Closure, we will just call it directly but // otherwise we'll resolve the pipes out of the container and call it with // the appropriate method and arguments, returning the results back out. return $pipe($passable, $stack); } elseif (!is_object($pipe)) { list($name, $parameters) = $this->parsePipeString($pipe); // If the pipe is a string we will parse the string and resolve the class out // of the dependency injection container. We can then build a callable and // execute the pipe function giving in the parameters that are required. $pipe = $this->getContainer()->make($name); $parameters = array_merge([$passable, $stack], $parameters); } else { // If the pipe is already an object we'll just make a callable and pass it to // the pipe as-is. There is no need to do any extra parsing and formatting // since the object we're given was already a fully instantiated object. $parameters = [$passable, $stack]; } return method_exists($pipe, $this->method) ? $pipe->{$this->method}(...$parameters) : $pipe(...$parameters); }; }; }
可以看到,最终运行返回的是一个闭包函数,引用了外部的两个变量,stack 和 pipe。其中 $stack 第一次执行的时候,它的值就是定义的 additionalHandle 方法中返回的闭包函数 (注意:此时并不是真的执行闭包函数里面的逻辑)。第二次执行的时候就是第一次执行返回的闭包函数,第三次执行的时候….. 以此类推。
至于 pipe 则是每次迭代时的中间件类,第一次就是上面定义的 count4 ? 为什么不是 count 1, 因为使用了 array_reverse 函数。有人会说,这个 carry 好复杂啊,迭代的时候我的脑子想下面的运行逻辑根本就想不下去。其实,在执行 array_reverse 函数的时候,你完全不用考虑下面的逻辑🙅♂️,每次迭代的时候仅仅只是返回一个变量,只是这个变量有点特殊,是个闭包函数。并不是直接执行里面的逻辑,等到真正调用这个变量 (闭包函数) 的时候才执行代码。我们来看一个简单的不能再简单的例子:
$res = function ($item) { return $item * 10; };
这个变量的值会是 2 吗?不会。它的值就是一个闭包函数,引用了一个外部的变量。既然值是闭包函数,那我可以调用吧:
$res(3)
这时候才是真正执行闭包函数的时候,传入了一个环境变量 3,执行闭包函数的逻辑,得到结果。
好了,现在我们可以知道 array_reverse 函数运行的整个流程。
第一下迭代:$stack 就是我们定义的 additionalHandle 闭包函数。pipe 就是第一个中间件 count4,
第二次迭代: $stack 是第一次执行返回的闭包函数,pipe 是 count3 中间件
第三次迭代: $stack 是第二次执行返回的闭包函数,pipe 是 count2 中间件
第四次迭代: $stack 是第三次执行返回的闭包函数,pipe 是 count1 中间件
现在迭代完毕,即 array_reverse 函数执行完毕,结果就是得到第四次迭代返回的闭包。然后运行这个闭包:
//$this->passable 就是$value值,还记得我们上面传入的是1 //$this->dispatcher(1, $pipes); return $pipeline($this->passable);
此时当前闭包 use 的就是 count1 这个类,然后我们可以开始看闭包里面的逻辑了,有三个分支
if (is_callable($pipe)) { return $pipe($passable, $stack); } elseif (!is_object($pipe)) { list($name, $parameters) = $this->parsePipeString($pipe); $pipe = $this->getContainer()->make($name); $parameters = array_merge([$passable, $stack], $parameters); } else { $parameters = [$passable, $stack]; }
对于我们现在的场景来说,走的是 else 分支。把初始值以及上一次迭代返回的闭包都存入 parameters 数组中。 为什么是走 else 分支,还记得我们这个示例是咋么操作的嘛:
$pipes = [(new Count1), (new Count2), (new Count3), (new Count4)]; $this->dispatcher(1, $pipes);
此时的 pipe 已然是个对象不需要去解析,前面的分支都和他没关系。继续往下走。
return method_exists($pipe, $this->method) ? $pipe->{$this->method}(...$parameters) : $pipe(...$parameters); /** protected $method = 'handle';
现在知道为什么你的中间件默认都需要 handle 方法了吧。查看类中的 handle 方法。
public function handle($value, \Closure $next) { $value += 1; var_dump('count1计算出来的值:' . $value); if ($value < 0) { return "系统异常"; } return $next($value); }
现在是到了第一个中间件核心处理的地方了,先简单的对变量进行处理,然后增加了过滤条件,如果不符合,直接打回去,这样就进不了下一层中间件了。也就是一开始说的那个 demo。如果要进入下一个中间件,那么就必须执行:
return $next($value);
为什么?因为上面说的 parameters 变量除了存储初始值之外,还存储了上一次迭代返回的闭包函数,此时你想进入下一层中间件,那么就必然要执行这个闭包函数,那为什么要 return ,这就是中间件的模型了,前置中间件处理完之后,到达 Application 然后开始执行后置中间件,一层层返回了。假设我们把 count 4 的 return 去掉,你应该能理解了吧。
class Count4 { //倒算税率值0.5 public function handle($value, \Closure $next) { $value *= 0.5; var_dump("count4 倒算税率值计算后的值:$value") . PHP_EOL; $next($value); } }
写到这里,也就分析的差不多了,一个小小的 array_reverse 函数,竟能玩出花。不得不佩服底层设计的巧妙。
结尾
最后,我很赞同 chongyi 在他的那篇文章说的一句话:要知道,再强大的 PHP 框架都是用 PHP 写出来的,本质上依旧是在一个大的基础上构建小型世界。