Laravel中间件详解

Laravel框架
594
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)之后即可实现后置中间件,检查顺序和中间件数组的顺序相反。

完结~