本文主要讲解中间件的执行机制。分为两个部分:
1、Larave
启动流程简单介绍,以便知道请求进来后,全局中间件在哪块生效。
2、Pipline
详解,由于中间件的执行流程最终由Pipline
实现,因此理清楚Pipline
的执行逻辑理也就清楚了中间件的执行逻辑,路由中间件同理。
一个中间件类大致如下:
<?php
namespace App\Http\Middleware;
use Closure;
class Test
{
public function handle($request, Closure $next)
{
$condition = true;
if (!$condition) {
throw new Exception("中间件校验失败,请检查");
}
// 必须,否则你的程序将不会有任何响应
return $next($request);
}
}
中间件是一个普通类,其中包含一个handle
可执行函数,如果要让程序继续往下执行,则在函数的最后一定要return $next($request)
,否则你的程序将没有任何响应。
这里有两个疑问:
1、$next($request)
是干嘛的?
2、前置/后置中间件是怎么实现的?
带着疑问我们来具体看下它的执行流程。
Laravel核心概念剖析 ),HTTP请求进来后交由App\Http\Kernel::class的handle处理,以下为handle的具体实现:public function handle($request)
{
// 开启HTTP REQUEST METHOD重写机制,以支持PUT、DELETE请求
$request->enableHttpMethodParameterOverride();
// 处理当前请求(核心函数)
$response = $this->sendRequestThroughRouter($request);
// 触发请求处理完成事件
$this->app['events']->dispatch(
new Events\RequestHandled($request, $response)
);
return $response;
}
// 处理请求的具体实现
protected function sendRequestThroughRouter($request)
{
$this->app->instance('request', $request);
Facade::clearResolvedInstance('request');
// 加载环境变量、加载配置、异常处理、注册门面模式、注册服务提供者、调用服务提供者boot等
$this->bootstrap();
return (new Pipeline($this->app))
->send($request)
// $this->app->shouldSkipMiddleware() // 是否禁用中间件
->through($this->app->shouldSkipMiddleware() ? [] : $this->middleware)
->then($this->dispatchToRouter());
}
由上面流程可知,请求最终由Laravel
的Pipline
执行。下面看下Pipline
的具体实现:
2、详解Pipeline
这里主要用到了三个函数,分别是:send
,through
和then
。
2.1、send
函数,设置当前请求
public function send($passable)
{
// 设置`$passable`为当前请求`$request`
$this->passable = $passable;
return $this;
}
2.2、through
函数,设置要通过的中间件列表
public function through($pipes)
{
// 设置`$pipes`为要通过的中间件列表`$this->middleware`
$this->pipes = is_array($pipes) ? $pipes : func_get_args();
return $this;
}
2.3 then
函数,执行中间件检查,处理请求
then
是Pipline
处理流程的核心,主要实现如下:
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 , mixed $initial = null ) : mixed
array_reduce
将回调函数 callback
迭代地作用到 array
数组中的每一个单元中,从而将数组简化为单一的值。参数解释:
参数 类型 解释 $array array 输入的 array $callback callable callback(mixed $carry, mixed $item): mixed
。carry
携带上次迭代的返回值; 如果本次迭代是第一次,那么这个值是 initial
。item
携带了本次迭代的值。 $initial mixed 如果指定了可选参数initial
,该参数将用作处理开始时的初始值,如果数组为空,则会作为最终结果返回。
嗯~,有点不好理解,下面看两个例子:
2.3.1 例子1
初识array_reduce
function test()
{
$arr = [1,2,3,4];
// return 返回最后一次迭代的结果
return array_reduce($arr, function ($carry, $item) {
echo 'carry' . '=' . $carry . '|' . 'item' . '=' . $item . PHP_EOL;
return $item;
}, 0);
}
echo test();
// 输出
carry=0|item=1
carry=1|item=2
carry=2|item=3
carry=3|item=4
// 最后一次迭代的结果
4
可见,第一次迭代的$carry
值是函数的第三个参数$initial
,$item
值为数组中的第一个元素1
,第二次迭代的$carry
值为第一次return
的值1
,$item
值为数组中的第二个元素2
,依此类推。
2.3.2 例子2
有了上面的简单理解之后,看个稍微复杂点的
$f1 = function (\Closure $callback) {
echo 'f1 start'.PHP_EOL;
$callback();
echo 'f1 end'.PHP_EOL;
};
$f2 = function (\Closure $callback) {
echo 'f2 start'.PHP_EOL;
$callback();
echo 'f2 end'.PHP_EOL;
};
$f3 = function (\Closure $callback) {
echo 'f3 start'.PHP_EOL;
$callback();
echo 'f3 end'.PHP_EOL;
};
$f4 = function () {
echo 'f4 执行结束'.PHP_EOL;
};
$action = array_reduce(array_reverse([$f1, $f2, $f3]), function ($carry, $item) {
return function () use ($carry, $item) {
return $item($carry);
};
}, $f4);
$action();
// 输出(此模型称为洋葱模型)
f1 start
f2 start
f3 start
f4 执行结束
f3 end
f2 end
f1 end
执行解析,由例子1可知:
第一次迭代结果:(因为有array_reverse
,所以第一次的$item
为f3
):
carry = f4,item = f3,因此第一次的return $item($carry) 就等于 return f3(f4)
第二次迭代结果:
carry = f3(f4),item = f2,因此第二次的return $item($carry) 就等于 return f2(f3(f4))
第三次迭代结果:
carry = f2(f3(f4)),item = f1,因此第三次的return $item($carry) 就等于 return f1(f2(f3(f4)))
获得最终结果为:
$action = return function () {
return f1(f2(f3(f4)));
}
执行步骤详解:
调用 $action()
相当于执行了f1()
, f1()
的参数为f2()
,因此执行f1()
时先输出【f1 start】,然后执行f1()
的$callback
参数f2
,输出【f2 start】,f2
的参数为f3
,因此执行$callback() = f3()
,输出【f3 start】,f3
的参数为f4
,执行$callback() = f4()
,输出【f4 执行结束】,f3
执行$callback()
后,接着执行下面的输出语句【f3 end】,f2,f1
同理,依次输出【f2 end、f1 end】。
为什么要array_reverse
?
由上面执行步骤可知:如果没有array_reverse
,执行顺序为f3、f2、f1、f4
,而array_reverse
之后的执行顺序为f1、f2、f3、f4
。
到这里array_reduce
相信大家都了解的差不多了,弄明白了array_reduce
的执行逻辑之后,接下来看Laravel
的Pipeline
的then
函数是怎么执行的:
// -----------------------------源代码-----------------------------
public function then(Closure $destination)
{
$pipeline = array_reduce(
array_reverse($this->pipes), $this->carry(), $this->prepareDestination($destination)
);
return $pipeline($this->passable);
}
// then主要依赖以下几个函数:
// $destination变量为此函数返回值:
protected function dispatchToRouter()
{
return function ($request) {
$this->app->instance('request', $request);
return $this->router->dispatch($request);
};
}
// 中间件走完之后查找路由逻辑
protected function prepareDestination(Closure $destination)
{
return function ($passable) use ($destination) {
return $destination($passable);
};
}
// carry (array_reduce的回调函数)
protected function carry()
{
return function ($stack, $pipe) {
return function ($passable) use ($stack, $pipe) {
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];
}
return method_exists($pipe, $this->method)
? $pipe->{$this->method}(...$parameters)
: $pipe(...$parameters);
};
};
}
// -----------------------------执行解析-----------------------------
// carry函数简化
function carry()
{
return function($stack, $pipe) {
if (is_callable($pipe)) {
return $pipe($passable, $stack);
} elseif(! is_object($pipe)) {
return function ($request) use ($stack, $pipe) {
list($name, $parameters) = $this->parsePipeString($pipe);
$pipe = $this->getContainer()->make($name);
$parameters = array_merge([$passable, $stack], $parameters);
return $pipe->handle(...$parameters);
};
}
};
}
// $this->prepareDestination($destination)拆解
function dispatchRouter($request)
{
return function ($request) {
$this->app->instance('request', $request);
return $this->router->dispatch($request);
}
}
// then实际就变成了
$pipeline = array_reduce(array_reverse($middlewares), carry(), dispatchRouter());
return $pipeline($request);
再来看下它的执行结果,这里假设
$middlewares = [AMiddleware::class, BMiddleware::class, CMiddleware::class]
第一次迭代:
$step1 = function ($request) use ($stack = function ($request) {
$this->app->instance('request', $request);
return $this->router->dispatch($request);
}, $pipe = CMiddleware::class) {
$parameters = array_merge([$passable, $stack], $parameters);
return $pipe->handle(...$parameters);
};
第二次迭代:
$step2 = function ($request) use ($stack = $step1, $pipe = BMiddleware::class) {
$parameters = array_merge([$passable, $stack], $parameters);
return $pipe->handle(...$parameters);
};
第三次迭代(最终结果):
$pipeline = function ($request) use ($stack = $step2, $pipe = AMiddleware::class) {
$parameters = array_merge([$passable, $stack], $parameters);
return $pipe->handle(...$parameters);
};
此时执行return $pipeline($request);
相当于执行了AMiddleware::class->handle
,该函数的第二个参数$next
对应$stack
,$stack = $step2
,所以handle
中的$next($request) = $step2($request)
,依次类推,最终$step1
中的$next($request)
执行分派路由操作。(逻辑同例子2)
接下来就可以回答文中的两个问题:
1、$next($request)
是干嘛的?
$next($request)
是执行下一个中间件的检查逻辑,如果没有$next($request)
则array_reduce
流程是不完整的,所以导致框架报参数错误。
2、前置/后置中间件怎么实现?
由于此模型为洋葱模型,所以将检查逻辑放在$next($request)
之前即可实现前置中间件,检查顺序为中间件数组的顺序,将检查逻辑放在$next($request)
之后即可实现后置中间件,检查顺序和中间件数组的顺序相反。
完结~