生命周期 5 管道流分析
管道流,也就是中间件过滤,代码初看很复杂,让我望而生畏,从而无法理解其中原理。经过一番修行,终于看懂了,然后又花了一整晚上,用自己的语言写了出来,很有成就感的说。。看我百万军中取上将首级!~~~(破音)
什么是洋葱?
上个图好理解一些。
代码位置
\Illuminate\Foundation\Http\Kernel::sendRequestThroughRouter:140
链式操作
return (new Pipeline($this->app))
->send($request)
->through($this->app->shouldSkipMiddleware() ? [] : $this->middleware)
->then($this->dispatchToRouter());
可以改写为:
//获得pipeline对象并设置属性,后面会用。
$pipeline = (new Pipeline($this->app)) //创建管道对象
->send($request) //设置$this->passable,要过滤的request对象
->through($this->app->shouldSkipMiddleware() ? [] : $this->middleware); //设置$this->pipes,要经过的过滤层。
//执行then方法
$pipeline->then($this->dispatchToRouter());
then方法执行了哪些操作:简单的说then方法创建了一个嵌套的闭包对象,然后将request传入这个闭包对象,在经过每个闭包层的时候,会去执行这个闭包层(中间件的)的handle方法,从而达到中间件过滤的效果。
$this->dispatchToRouter方法代码(此段代码function里面的东西在最后才会执行):
/**
* Get the route dispatcher callback.
*
* @return \Closure
*/
protected function dispatchToRouter()
{
return function ($request) {
$this->app->instance('request', $request);
return $this->router->dispatch($request);
};
}
Get the route dispatcher callback. 意思是获得路由分发器的回调函数,既然是获得函数,那么就不会执行里面的回调函数,只是执行了return 语句,返回一个回调函数,回调函数本身不执行。
因此,$pipeline->then($this->dispatchToRouter());
可以看成如下代码:
$pipeline->then(function ($request) {
$this->app->instance('request', $request);
return $this->router->dispatch($request);
});
$pipeline->then
方法代码:
/**
* Run the pipeline with a final destination callback.
*
* @param \Closure $destination
* @return mixed
*/
public function then(Closure $destination)
{
$pipeline = array_reduce(
array_reverse($this->pipes), $this->carry(), $this->prepareDestination($destination)
);
return $pipeline($this->passable);
}
Run the pipeline with a final destination callback.
这句话的意思是运行pipeline,并指定一个最终的目标回调函数,也就是我们传入的$destination
变量,也就是$this->dispatchToRouter()
,这里就叫它「大BOSS」。
可以看到then方法中进行了两大步操作:
- 使用
array_reduce
函数将所有的管道倒序传入$this->carry()
这个回调函数,最后再到大BOSS那里。整个过程最终形成了类似洋葱的多层关卡,只有不断通关,才能见到大BOSS,决一死战。 - 而
return $pipeline($this->passable)
就是整个通关的过程,并返回一个通关结果。
接下来详细描述一下关卡形成过程和通关过程.
通关之前,先去此地打怪修炼。
关卡形成过程
三个参数分析
在then方法中
$pipeline = array_reduce(
array_reverse($this->pipes), $this->carry(), $this->prepareDestination($destination)
);
首先有7道关卡的名字(默认5道,另外两道是第三方包添加的中间件),这些关卡都是全局中间件,即$this->pipes
,内容如下:
array (
0 => 'Dingo\\Api\\Http\\Middleware\\Request',
1 => 'App\\Http\\Middleware\\CheckForMaintenanceMode',
2 => 'Illuminate\\Foundation\\Http\\Middleware\\ValidatePostSize',
3 => 'App\\Http\\Middleware\\TrimStrings',
4 => 'Illuminate\\Foundation\\Http\\Middleware\\ConvertEmptyStringsToNull',
5 => 'App\\Http\\Middleware\\TrustProxies',
6 => 'Barryvdh\\Debugbar\\Middleware\\InjectDebugbar',
)
array_reverse($this->pipes)
倒序,就是:
array (
0 => 'Barryvdh\\Debugbar\\Middleware\\InjectDebugbar',
1 => 'App\\Http\\Middleware\\TrustProxies',
2 => 'Illuminate\\Foundation\\Http\\Middleware\\ConvertEmptyStringsToNull',
3 => 'App\\Http\\Middleware\\TrimStrings',
4 => 'Illuminate\\Foundation\\Http\\Middleware\\ValidatePostSize',
5 => 'App\\Http\\Middleware\\CheckForMaintenanceMode',
6 => 'Dingo\\Api\\Http\\Middleware\\Request',
)
为什么要倒序?因为关卡从内往外开始形成的,大BOSS在最里面,最外层的是最后才形成,因此Dingo\\Api\\Http\\Middleware\\Request
跑到了第一关,Barryvdh\\Debugbar\\Middleware\\InjectDebugbar
是第7关,大BOSS在第8关。
$this->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) {
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));
}
};
};
}
Get a Closure that represents a slice of the application onion.
获得一个闭包代表洋葱型应用程序的一层。意思就是我们游戏的一个关卡了。这个也是获得闭包,那么同上面的大BOSS关一样,它也是只执行第一个return语句,返回一个关卡,内容为:
function ($stack, $pipe) {
return function ($passable) use ($stack, $pipe) {
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));
}
};
};
而根据我们官方文档array_reduce
的$callback
方法签名参数的说明:
mixed callback ( mixed $carry , mixed $item )
- carry 携带上次迭代里的值; 如果本次迭代是第一次,那么这个值是 initial。
- item 携带了本次迭代的值.
由此对照可知,在创建第1关(第1次迭代)时,carry()方法中的return function ($stack, $pipe)
中的$stack
就应该是then方法中的第三参数$this->prepareDestination($destination)
,而$pipe
就是第1关的名字Dingo\\Api\\Http\\Middleware\\Request
。
而$this->prepareDestination
方法的代码如下
/**
* Get the final piece of the Closure onion.
*
* @param \Closure $destination
* @return \Closure
*/
protected function prepareDestination(Closure $destination)
{
return function ($passable) use ($destination) {
try {
return $destination($passable);
} catch (Exception $e) {
return $this->handleException($passable, $e);
} catch (Throwable $e) {
return $this->handleException($passable, new FatalThrowableError($e));
}
};
}
Get the final piece of the Closure onion.
获得闭包洋葱的最后一片,这不就是大BOSS么?
它返回一个闭包,也就是只执行第一个return语句。相当于大BOSS关为:
function ($passable) use ($destination) {
try {
return $destination($passable);
} catch (Exception $e) {
return $this->handleException($passable, $e);
} catch (Throwable $e) {
return $this->handleException($passable, new FatalThrowableError($e));
}
}
而这个$destination
就是我们一开始看到的
function ($request) {
$this->app->instance('request', $request);
return $this->router->dispatch($request);
}
关键点
首先必须要理解array_reduce
的思想,通过遍历第一参数array
中的值,每次都去执行第二参数callback
。而每次执行callback
时,执行的结果都作为下一次执行callback
的第一参数,而遍历的值都作为每次执行callback
的第二参数。也就是说类似于如下代码:
function array_reduce($pipes, $callback, $initial=null)
{
$stack = $initial;
foreach($pipes as $pipe){
$stack = $callback($stack, $pipe);
}
return $stack;
}
形成具体过程
我们这里的$this->carry()
,它每次的操作都仅仅是返回了一个待执行的闭包对象。注意特别注意:每次都使用use引入静态变量,每次的$stack, $pipe都是固定的具体值嵌入这个闭包对象中。
function ($passable) use ($stack, $pipe) {
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));
}
}
每次返回闭包对象后,都放到下一次的$stack
参数中。因此,第1次迭代时,返回的闭包对象就是「大BOSS关|倒数第1关」,伪代码如下:
function ($passable) use (「大BOSS」, '倒数第1关名字') {
try {
$slice = parent::carry();
$callable = $slice(「大BOSS」, '倒数第1关名字');
return $callable($passable);
} catch (Exception $e) {
return $this->handleException($passable, $e);
} catch (Throwable $e) {
return $this->handleException($passable, new FatalThrowableError($e));
}
}
这个结果会直接赋给在第二次迭代的$stack
参数,那么第2次迭代返回的结果就是「大BOSS关|倒数第1关|倒数第2关」:
function ($passable) use (「大BOSS关|倒数第1关」, '倒数第2关名字') {
try {
$slice = parent::carry();
$callable = $slice(「大BOSS关|倒数第1关」, '倒数第2关名字');
return $callable($passable);
} catch (Exception $e) {
return $this->handleException($passable, $e);
} catch (Throwable $e) {
return $this->handleException($passable, new FatalThrowableError($e));
}
}
这个结果会直接赋给在第3次迭代的$stack
参数,那么第3次迭代返回的结果就是「大BOSS关|倒数第1关|倒数第2关|倒数第3关」:
function ($passable) use (「大BOSS关|倒数第1关|倒数第2关」, '倒数第3关名字') {
try {
$slice = parent::carry();
$callable = $slice(「大BOSS关|倒数第1关|倒数第2关」, '倒数第3关名字');
return $callable($passable);
} catch (Exception $e) {
return $this->handleException($passable, $e);
} catch (Throwable $e) {
return $this->handleException($passable, new FatalThrowableError($e));
}
}
以此类推,
最后形成的关卡集群为:
return function ($passable) use (「大BOSS关|倒数第1关|倒数第2关|倒数第3关|倒数第4关|倒数第5关|倒数第6关」, '倒数第7关名字') {
try {
$slice = parent::carry();
$callable = $slice(「大BOSS关|倒数第1关|倒数第2关|倒数第3关|倒数第4关|倒数第5关|倒数第6关」」, '倒数第7关名字');
return $callable($passable);
} catch (Exception $e) {
return $this->handleException($passable, $e);
} catch (Throwable $e) {
return $this->handleException($passable, new FatalThrowableError($e));
}
}
这就是array_reduce
形成的一个嵌套多层的超级大闭包(洋葱),超级牛逼(头痛)。
所以说,要学好英语,stack不就是栈的意思么,小小一个参数其实是一个超大的栈。
好了,现在终于知道$pipeline
就是上面这个关卡集群了。
冲关过程
$pipeline($this->passable)
$pipeline
是一个闭包对象,那么上面这种写法就是去执行它,传入的是$this->passable
,也就是IlluminateRequest
对象。
这次执行,首先到了$slice = parent::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)) {
[$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];
}
$response = method_exists($pipe, $this->method)
? $pipe->{$this->method}(...$parameters)
: $pipe(...$parameters);
return $response instanceof Responsable
? $response->toResponse($this->container->make(Request::class))
: $response;
};
};
}
看起来非常吓人,但一切反动派都是纸老虎,实际上这个只会执行第一个return,也就是说$slice
实际上得到的是一个闭包对象。
下面这句就是执行这个闭包对象,这里的$stack
就是关卡栈(管道流)
$callable = $slice($stack, $pipe)
也就是
$callable = $slice(「大BOSS关|倒数第1关|倒数第2关|倒数第3关|倒数第4关|倒数第5关|倒数第6关」, '倒数第7关名字');
这里的$callable
也是闭包,而执行这个闭包对象,也只是执行上面这个carry方法的第二个return,还是返回一个闭包对象。
然后再执行它。
return $callable($passable)
好了,到了这里,终于开始正式的执行冲关操作了。在我们这个上下文中,传入$pipe
的都是字符串,因此都是在执行下面的操作:
//通过$pipe名字拆分$name和$parameters
[$name, $parameters] = $this->parsePipeString($pipe);
//解析中间件对象
$pipe = $this->getContainer()->make($name);
//将request对象,关卡栈对象,$parameters进行array_merge操作并赋给$parameters变量,它是一个数组。
$parameters = array_merge([$passable, $stack], $parameters);
接下来,就是看中间件对象中有没有handle方法,有就执行handle方法。并且使用...$parameters
这种写法将一个数组变量拆分为方法中的参数序列。
$response = method_exists($pipe, $this->method)
? $pipe->{$this->method}(...$parameters)
: $pipe(...$parameters);
这样,$passable
就是中间件中$request
参数,而$stack
就是$next
。
下面是DingoRequest中间件的handle代码:
public function handle($request, Closure $next)
{
try {
if ($this->validator->validateRequest($request)) {
$this->app->singleton(LaravelExceptionHandler::class, function ($app) {
return $app[ExceptionHandler::class];
});
$request = $this->app->make(RequestContract::class)->createFromIlluminate($request);
$this->events->fire(new RequestWasMatched($request, $this->app));
return $this->sendRequestThroughRouter($request);
}
} catch (Exception $exception) {
$this->exception->report($exception);
return $this->exception->handle($exception);
}
return $next($request);
}
可以看到$request
对象会经历很多磨难。如果一切正常,就会执行到return $next($request)
。
而这里的$next
就是我们的关卡栈对象$stack
。$next($request)
就是执行一次闭包,也就是拆包(剥皮/冲关)。因此,每执行一次,层数就少一层。为什么?因为我们前面构建关卡的时候,每个关卡的$stack
参数都是固定的,静态的,越执行到后面,$stack
剩下的关卡就越少了!
好了,经过层层磨难,终于见到大boss了。。
执行到了下面这句:
return $destination($passable);
别忘了,$passable
是request对象,而$destination
是个闭包对象,就是我们一开始的那个
/**
* Get the route dispatcher callback.
*
* @return \Closure
*/
protected function dispatchToRouter()
{
return function ($request) {
$this->app->instance('request', $request);
return $this->router->dispatch($request);
};
}
因此,执行这个闭包,首先执行到$this->app->instance('request', $request);
,注册request对象到容器(百宝箱)的instances(实例缓存)属性中。
然后就是$this->router->dispatch($request)
,将request通过路由到达应用程序,然后返回最终结果了!
小结
至此,管道流已经大致理解了。
- 在看这个代码前,必须对闭包,array_reduce,laravel生命周期有一定理解。
- 学好英语,看注释,看变量命名,方法命名,往往能帮助理解。
- 有一个好的IDE,可以在不理解的情况下先有一个直观印象,从而帮助理解。
本作品采用《CC 协议》,转载必须注明作者和本文链接
此帖子中有些表述仍然觉得不太清晰,更清晰的表述可以见下下个帖子--《路由&控制器中间件过滤》。
推荐文章: