Pipeline 管道操作实现请求中间件过滤(最详细讲解)
简介
通过 8 章的内容,我们终于或多或少,或粗略或详细的把 Laravel 引导启动给写完了。无论内容详细与否,相信看到的童鞋,一定会有所收获。Laravel 框架为什么深受欢迎与追捧,我想与它严谨的程序工程设计思想分不开,就好比我们的电脑要启动,一定有引导和初始化;Laravel 在这方面从不懈怠,我们能从中学到很多。
上面一堆废话,大家左眼进右眼就行啦哈。下面我们就进入 Laravel 中最牛逼也是最核心的操作之一: Pipeline 管道操作。
为什么说他牛逼呢,因为它用了一卡车的闭包和回调,如果学会了,对 PHP 闭包的理解就会进入一个更深的层次。
Pipeline 管道操作大体过程
我们先从管道操作开始的代码看起
protected function sendRequestThroughRouter($request)
{
$this->app->instance('request', $request);
Facade::clearResolvedInstance('request');
// 前 8 章,就围绕这个简单 bootstrap 方法,疯狂展开。。。
$this->bootstrap();
// 这一节我们开始疯狂展开下面的这些代码
return (new Pipeline($this->app))
->send($request)
->through($this->app->shouldSkipMiddleware() ? [] : $this->middleware)
->then($this->dispatchToRouter());
}
看这个链式调用,我们捋一下思路。首先实例化管道(Pipeline)类,将容器对象传入构造函数,返回管道对象;然后调用管道对象的 send(送) 方法,以 request(请求) 对象为参数(即把 requset 送入管道,准备穿透中间件);再然后,调用对象的 through(穿过)方法,以中间件数组为参数;最后,如果请求(经过过滤的)成功过来了,调用管道对象的 then 方法,将请求派遣分配的路由中。
注:在路由中,还有一套中间件过滤,其中一部分就是我们自行定义的。上面这部分中间件过滤,是对全局请求的过滤。当路由中的中间件过滤完毕后,就进入我们定义的控制器部分了。
下面我们进入展开细看阶段
第一步,实例化管道类,主要看构造函数
public function __construct(Container $container = null)
{
$this->container = $container;
}咳咳!!(尴尬),这个好简单。。。
第二步,调用 send 方法,以 request 为参
public function send($passable)
{
$this->passable = $passable;
return $this;
}这个额,注意
$this->passable
就是request
对象,以后看到,要注意。第三步,调用 through 方法
调用之前,我们先会执行参数位置的三元运算,结果返回的是
$this->middleware
,因为这个三元判断的是中间件操作是否有关闭,默认是开启的哈,也一定会开启,关闭那不是扯淡吗public function through($pipes)
{
$this->pipes = is_array($pipes) ? $pipes : func_get_args();
return $this;
}注意:
$this->pipes
就是上面的$this->middleware
,他们是数组类型,我们来看一下这个数组有什么吧protected $middleware = [
\App\Http\Middleware\CheckForMaintenanceMode::class,
\Illuminate\Foundation\Http\Middleware\ValidatePostSize::class,
\App\Http\Middleware\TrimStrings::class,
\Illuminate\Foundation\Http\Middleware\ConvertEmptyStringsToNull::class,
\App\Http\Middleware\TrustProxies::class,
];看到没,就是 5 个类,待会请求会穿透这个类。具体穿透方式就是:实例化 5 个类,依次倒序(重点!!!倒序)调用实例化的 handle 方法。
第四步,调用 then 方法,以
$this->dispatchToRouter()
返回值为参最后一步是最重要,也是最难理解的一步。因为里面涉及实现穿透中间件的原理的实现方式,主要利用返回多个闭包,逐次循环调用,比较难以理解。我会尽最大可能讲解明白。下面我们用新章节段单独讲解
then 方法里面到底有什么
先看源码
public function then(Closure $destination) // $destination 参数是 $this->dispatchToRouter() 返回的闭包
{
// 下面这个 array_reduce 函数(PHP内置的),是实现穿透的主要函数。这个函数作用详解下面单独讲。
$pipeline = array_reduce(
// array_reverse 函数(PHP内置)作用很简单:就是将索引数组的顺序颠倒一下。
array_reverse($this->pipes), $this->carry(), $this->prepareDestination($destination)
);
// 执行请求穿透。
return $pipeline($this->passable);
}
先简单说一下:
上面方法的第一段
$pipeline = array_reduce(...
用形象一点说法就是:将最终出口用洋葱皮一层一层的包起来。上面方法的第二段
return $pipeline($this->passable)
用形象一点的说法就是:用请求穿透洋葱皮,然后洋葱皮一层一层的被剥落,最终请求出去了(进入程序下一段)
$destination 到底是什么鬼
我们先看一下参数
$this->dispatchToRouter()
返回的是什么鬼吧protected function dispatchToRouter()
{
return function ($request) {
$this->app->instance('request', $request);
return $this->router->dispatch($request);
};
}明白了,返回了一个函数(也称闭包对象),这个函数需要一个参数(实际就是那个请求对象)。这个函数还不执行,只是当做一个变量被扔沙包一样,扔来扔去,它肯定想:我什么时候能爆发我的小宇宙(执行内部的代码)。所以,我们作为人,现在可以完全不理 function 里面是什么鬼。我们就知道它是个沙包(参数),可以传来传去。最后,如果我们看到这个沙包在某一个地方后面出现一对小括号,并且里面有个参数(请求对象),这时候我们知道它的小宇宙爆发,我们可以进入这个小宇宙一探究竟了。
array_reduce 内置函数的作用。。
官方说明:用回调函数迭代地将数组简化为单一的值。
这句话什么鬼,我怎么这么晕,我在哪,我要去哪里????
不用担心我们先换句形象的一段内容:
大家都知道洋葱是什么吧。洋葱皮是一层一层的大家没有意见吧。剥洋葱的动作大家都能想象吧(不就是一层一层剥皮吧)。假设现在我们对剥洋葱进行逆操作:首先我们得有一个洋葱芯($destination),然后还得有洋葱皮们(颠倒后的
$this->pipes
);然后我们还得需要包葱皮操作呀:其实就是上面的$this->carry()
方法的返回值(注意是返回值,不是$this->carry()
本身,是它的返回值),我们知道 array_reduce 函数的包葱皮操作是一个沙包(这个沙包会在 array_reduce 函数体内爆发,执行包这个动作),所以,$this->carry()
返回值,一定是一个沙包;我们看 PHP 官方文档的时候知道这个实现了包葱皮的沙包,需要两个参数$carry
和$item
,这两个是什么呢?$carry
就是我们手上的洋葱(它正在被洋葱皮包起来),$item
就是我们手上的洋葱皮(它从数组中逐次拿出来的,然后一层层包在我们手上的洋葱上)。那么数组的洋葱皮用完的,我们会得到什么?这不是废话吗,当然得到已经包好的洋葱啦。这就是上面官方说的:用回调函数(包葱皮动作)迭代地将数组(洋葱皮们)简化为单一的值(最后包好的洋葱)进一步细讲:关于洋葱芯($destination)还需要做进一步加工才能作为芯被皮包起来。这个加工就是
$this->prepareDestination($destination)
,我们看一下源码protected function prepareDestination(Closure $destination)
{
return function ($passable) use ($destination) {
return $destination($passable);
};
}哇。。又返回一个沙包。。。这个沙包有个 use 哎,哦我知道了,就是小宇宙中加上外部来的神奇元素,可以让爆发更牛逼一些。行了,看着逻辑我知道了,这是一个连锁爆发。即:如果返回的这个沙包爆发了(后面跟上小括号),内部的另一个沙包
$destination
紧接着爆发。这个内部的$destination
沙包原来就是 use 引进来的神奇元素呀。。原来如此。管它呢现在经过
$this->prepareDestination($destination)
加工返回就是一个沙包,它没爆发,先不管它里面是什么鬼。。我们来看包洋葱动作吧。。
!! carry 方法是子类的方法!!
是
Illuminate\Routing\Pipeline
类,而非父类Illuminate\Pipeline\Pipeline
!!protected function carry()
{
return function ($stack, $pipe) { // 这句 return 返回了 array_reduce 所需的闭包参数。此闭包将在 array_reduce 中被执行。
return function ($passable) use ($stack, $pipe) { // 上一个闭包在 array_reduce 中被执行后,返回这个闭包。这个才是真正被洋葱皮不断包着的洋葱。
try {
$slice = parent::carry();
$callable = $slice($stack, $pipe);
return $callable($passable);
} catch (Exception $e) {
return $this->handleException($passable, $e);
} catch (Throwable $e) {
return $this->handleException($passable, new FatalThrowableError($e));
}
};
};
}还记得我刚才说的吧,是
$this->carry()
返回值是真正包洋葱的动作,即return function ($stack, $pipe) {...}
这个沙包。上面我说过,包洋葱动作需要两个参数,一个是正在包的洋葱($stack),一个是洋葱皮($pipe)。那么,经过 array_reduce 函数执行包这个动作(爆发沙包的小宇宙),实际就是调用
function ($stack, $pipe) {...}
函数,这个函数的返回值就是包好当前洋葱皮的洋葱,而这个洋葱将参与下一个包葱皮的行为,这个行为同样是这个闭包。下面我们看一下怎么包的。第二行代码就是包的动作
return function ($passable) use ($stack, $pipe) {...}
返回了一个沙包(真正的洋葱),从沙包的参数和引入的神奇变量我们知道,当洋葱被一层层剥落的时候,执行的动作与$passable
(请求对象)、$stack
(剥去当前层葱皮的洋葱)、$pipe
(中间件)三个参数有关。我就明说吧,大家在中间件 handle 方法中所看到的
$request
、$next
对应的就是$passable
和$stack
。那么$pipe
跑哪去了。呵呵,$pipe
就是 handle 方法所在的类呀。。现在我们看一眼把
$this->middleware
(那 5 个洋葱皮)包好的洋葱的数据结构下面我们来看一下剥洋葱的过程
return $pipeline($this->passable);
是从这里开始的。$pipeline 是最终包好那个洋葱,这是一个沙包,哇。后面跟了一个小括号,爆发啦。。。爆发代码:
!! 从子类的 carry 方法开始爆发!!
protected function carry()
{
return function ($stack, $pipe) { // 上面讲了,这个闭包会在 array_reduce 被执行掉
return function ($passable) use ($stack, $pipe) { // 而这个就通过 return $pipeline($this->passable) 以及 $next($request) 被不断的执行掉,从而不断过滤 $request 请求。
// 爆发从这里开始。。。
try {
// 这个才是调用父类 carry 方法的地方,返回的是 return function ($stack, $pipe) {...}
$slice = parent::carry();
// 这行执行了 function ($stack, $pipe) {...},返回了 function ($passable) use ($stack, $pipe) {...}
$callable = $slice($stack, $pipe);
// 而这行执行了 function ($passable) use ($stack, $pipe) {...} ,从而在父类中爆发
return $callable($passable);
} catch (Exception $e) {
return $this->handleException($passable, $e);
} catch (Throwable $e) {
return $this->handleException($passable, new FatalThrowableError($e));
}
};
};
}父类 carry!!
protected function carry()
{
return function ($stack, $pipe) {
return function ($passable) use ($stack, $pipe) {
// 承接子类的 return $callable($passable); 从这爆发
// 这里判断 洋葱皮 是沙包吗,明显不是,跳过
if (is_callable($pipe)) {
return $pipe($passable, $stack);
// 这里判断 洋葱皮 不是对象吗,明显不是,执行(洋葱皮只是一段字符串,一个类全名的字符串,需要实例化的)
} elseif (! is_object($pipe)) {
list($name, $parameters) = $this->parsePipeString($pipe);
// 实例化洋葱皮
$pipe = $this->getContainer()->make($name);
// 定义好 handle 方法需要的参数,请求对象,和下一层洋葱
$parameters = array_merge([$passable, $stack], $parameters);
} else {
$parameters = [$passable, $stack];
}
// 呵呵,控制器操作全在这一行里面,当然想进入控制器,首先剥完当前洋葱。还要把路由的洋葱剥完。后面的先不管,现在只知道,我们请求的响应内容从这里获取的。。。。这行代码,首先判断我这个洋葱皮对象有没有 handle 方法,有的话用上面准备好的参数执行这个方法。另一种说法,就是剥夺掉这个洋葱皮
$response = method_exists($pipe, $this->method)
? $pipe->{$this->method}(...$parameters)
: $pipe(...$parameters);
return $response instanceof Responsable
? $response->toResponse($this->container->make(Request::class))
: $response;
};
};
}
额外补充一点。我们在中间件 handle 方法中看到的 $next
参数,之前我说过是 剥好当前洋葱皮的洋葱。在 handle 方法最后一定有一个 return $next($request);
是不是。你看看哈,和上面 return $pipeline($this->passable);
不就是一个意思。通过这种 一层一层地剥落我的洋葱,把过来的请求过滤各遍。
写在最后
最后没什么可写的了,如果还有什么疑惑。留言询问。记得点播赞。我认为这是到现在讲 管道操作 最详细的教程啦。。。感觉学到的,点赞点赞点赞,重要事说三遍。。。
额外补充
关于为什么要用 array_reverse 函数倒排一下 $this->pipes
,这是因为,包洋葱的时候,是从数组第一个元素开始拿葱皮,而执行请求穿透(剥洋葱)是从数组最后一个元素开始剥。这就如同栈一样,后进先出。而倒排一下 $this->pipes
,最终实现穿透顺序,就是没有倒序 $this->pipes
时的顺序,大家想一下是不是这样。因为后进先出,倒排一下,就变成数组什么顺序,穿透时就是什么顺序。