本文主要讲解中间件的执行机制。分为两个部分:
1、Larave
启动流程简单介绍,以便知道请求进来后,全局中间件在哪块生效。
2、Pipline
详解,由于中间件的执行流程最终由Pipline
实现,因此理清楚Pipline
的执行逻辑理也就清楚了中间件的执行逻辑,路由中间件同理。
一个中间件类大致如下:
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)
之后即可实现后置中间件,检查顺序和中间件数组的顺序相反。
完结~