Laravel中间件详解

Laravel框架
634
0
0
2022-10-21
标签   Laravel基础

本文主要讲解中间件的执行机制。分为两个部分:

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());
}

由上面流程可知,请求最终由LaravelPipline执行。下面看下Pipline的具体实现:

2、详解Pipeline

这里主要用到了三个函数,分别是:send,throughthen

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 函数,执行中间件检查,处理请求

thenPipline处理流程的核心,主要实现如下:

public function then(Closure $destination)
{
$pipeline = array_reduce(
array_reverse($this->pipes), $this->carry(), $this->prepareDestination($destination)
);
return $pipeline($this->passable);
}

完事。

嗯~,看起来很简单嘛,就两行代码。:sweat_smile:但是理解起来可能有点费劲,一起来看下。这个核心在对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): mixedcarry携带上次迭代的返回值; 如果本次迭代是第一次,那么这个值是 initialitem携带了本次迭代的值。 $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,所以第一次的$itemf3):

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的执行逻辑之后,接下来看LaravelPipelinethen函数是怎么执行的:

// -----------------------------源代码-----------------------------
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)之后即可实现后置中间件,检查顺序和中间件数组的顺序相反。

完结~