laravel框架的中间件实现原理

代码是laravel8.57里的,其它版本应该差异不会太大吧

​
    protected function sendRequestThroughRouter($request)
    {
        $this->app->instance('request', $request);
​
        Facade::clearResolvedInstance('request');
​
        $this->bootstrap();
​
        return (new Pipeline($this->app))
                    ->send($request)
                    ->through($this->app->shouldSkipMiddleware() ? [] : $this->middleware)
                    ->then($this->dispatchToRouter());
    }
​

这个是 src/Illuminate/Foundation/Http/Kernel.php的代码片断,默认的中间件是app/Http/Kernel.php中的

 protected $middleware = [
        // \App\Http\Middleware\TrustHosts::class,
        \App\Http\Middleware\TrustProxies::class,
        \Fruitcake\Cors\HandleCors::class,
        \App\Http\Middleware\PreventRequestsDuringMaintenance::class,
        \Illuminate\Foundation\Http\Middleware\ValidatePostSize::class,
        \App\Http\Middleware\TrimStrings::class,
        \Illuminate\Foundation\Http\Middleware\ConvertEmptyStringsToNull::class,
    ];

然后then方法就是在执行了:

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, $initial = null): mixed 将回调函数 callback 迭代地作用到 array 数组中的每一个单元中,从而将数组简化为单一的值。$callback的函数为callback(mixed $carry, mixed $item): mixed $carray为对$array中的上一个执行$callback回调函数的结果,$item为下一个$array中的元素

也就是先对$array中的第一个元素执行$callback函数,将结果对第二元素执行$callback时的第一个参数即$carray,第二个元素为$callback的第二个参数即$item,直到每个元素执行完。由于第一次执行时是没有上一次执行的结果的,所以 可以传一个 $initial参数,即为第一次执行$callback的$carry参数。如果不传$initial的话第一次执行$callback时的$carray就为null,就需要在$callback手动判断处理。

如果还不好理解的话,文档的评论里有个人写了个等价的函数(或者说实现的代码本体?)可以帮助理解,下面的略微改了下变量名,话说没想到这个函数居然存在得这么早,以前真的是完全没用过。。。。

function array_reduce($array, $callback, $initial=null)
{
    $carray = $initial;
    foreach($array as $item) {
        $carray = $callback($carray, $item);
    }
    return $carray;
}

而框架中的实现么,$callback为

protected function carry()
    {
        return function ($stack, $pipe) {
            return function ($passable) use ($stack, $pipe) {
                try {
                    if (is_callable($pipe)) {
                        return $pipe($passable, $stack);
                    } elseif (! is_object($pipe)) {
                        [$name, $parameters] = $this->parsePipeString($pipe);
​
                        $pipe = $this->getContainer()->make($name);
​
                        $parameters = array_merge([$passable, $stack], $parameters);
                    } else {
                        $parameters = [$passable, $stack];
                    }
​
                    $carry = method_exists($pipe, $this->method)
                                    ? $pipe->{$this->method}(...$parameters)
                                    : $pipe(...$parameters);
​
                    return $this->handleCarry($carry);
                } catch (Throwable $e) {
                    return $this->handleException($passable, $e);
                }
            };
        };
    }

$inital为

protected function prepareDestination(Closure $destination)
    {
        return function ($passable) use ($destination) {
            try {
                return $destination($passable);
            } catch (Throwable $e) {
                return $this->handleException($passable, $e);
            }
        };
    }

这个粗一看闭包的层级有点儿多,不是太好看,不是很好理解,下面弄成个精简版模型来说明

class p implements ArrayAccess
{
    public array $data = [];
​
    public function offsetExists($offset)
    {
        return $this->data[$offset];
    }
​
    public function offsetGet($offset)
    {
        return $this->data[$offset];
    }
​
    public function offsetSet($offset, $value)
    {
        if ($offset === null) {
            return $this->data[] = $value;
        }
        return $this->data[$offset] = $value;
    }
​
    public function offsetUnset($offset): void
    {
        unset($this->data[$offset]);
    }
}
​
class a
{
    public function call(p $param, $next): void
    {
        echo 'class a now calling', PHP_EOL;
        print_r($param->data);
        $param[] = 'a';
        $next($param);
        echo 'class a now called', PHP_EOL;
        print_r($param->data);
    }
}
​
class b
{
    public function call(p $param, $next)
    {
        echo 'class b called ', PHP_EOL;
        print_r($param->data);
        $param[] = 'b';
        return $next($param);
    }
}
​
class c
{
    public function cc(p $param): void
    {
        echo 'I am initial class but latest called', PHP_EOL;
        print_r($param->data);
        $param[] = 'c';
        echo PHP_EOL;
    }
}
​
$aa = new a();
$bb = new b();
$cc = new c();
$middlewares = [
    [$aa, 'call'], [$bb, 'call'],
];
​
​
$fn = function ($forwardRes, $nextFn) {
    return function ( $param) use ($forwardRes, $nextFn) {
        return $nextFn($param, $forwardRes);
    };
};
​
$a = array_reduce(array_reverse($middlewares), $fn, [$cc, 'cc']);
$a(new p());

为了方面后面的说明就把类a,b,c的三个类call方法作为中间件的执行方法,$callback中的$carry和$item用$forwardRes和$nextFn代替,使用ArrayAccess代替普通数组更方便作为参数。$fn的作用不仅是将$middlewares中的每个callable元素包装成一个闭包函数,执行把把这个闭包在作为每个callable中的$next参数,即执行$next闭包就是在执行下一个中间件;而且也把整个$middleware数组最终生成为

function ( $param)  {
        return $nextFn($param, $forwardRes);
}

这个一个闭包。

下面是执行array_reduce时情况

第一次执行$fn时

$forwardRes为c对象的cc方法的闭包,因为调用过array_reverse,所以$nextFn为b对象的call方法,生成了一个执行时就执行b对象call方法的闭包,相当于

function ( $param)  {
        return b::call($param, $forwardRes);
}

此时的$forwardRes即为 c对象的cc方法。

然后是第二次执行

hfPnts.png

第二次执行生成了$a一个执行时就执行a对象的call方法的闭包函数,相当

function ( $param)  {
        return a::call($param, $forwardRes);
}

此时的$forwardRes即为前一个生成执行b::call的闭包函数。

最后执行$a,即先执行a对象的call方法

hfk9h9.png

如图此时$next即为第一次array_reduce执行时生成的闭包,

function ( $param)  {
        return b::call($param, $forwardRes);
}

然后a的call中调用$next($param),就是执行那个闭包

hfVtAO.png

从而执行b的call方法

hfVhgs.png

如此往复执行,直到执行最开始传入的callable即c对象的cc方法后层层返回

整个调用流程就相当于a调用b,b中调用c

hfZQIS.png

执行输出结果为

class a now calling
Array
(
)
class b called
Array
(
  [0] => a
)
I am initial class but latest called
Array
(
  [0] => a
  [1] => b
)

class a now called
Array
(
  [0] => a
  [1] => b
  [2] => c
)

可以看出来整个执行过程严重依赖于$middlewares中的元素顺序,如果不调用array_reverse的话就是b调a,a再调c了,但$initial都是最后执行的,除了如a对象的call方法那种写法外,因为正常一般都是直接return $next($param)。真正处理中间件的业务逻辑可以写在$next方法之前或者之后,但一定要执行$next方法,不然后整个执行就会断掉。

然后laravel中的要更麻烦一些,主要注册的中间件都是类名,还得通过容器实例化为对象再调用对象的中handle方法,最后执行route中的run方法什么到controller里的方法,再将结果交给response处理,也即是$initial就是处理解析route的方法dispatchToRouter,那个$param就是request实例,最终返回的为response实例。

这里再介绍一种在go语言里gin框架实现这种层层调用中间件的原理:

class middleware
{
    protected array $calls = [];
​
    protected $i = -1;
    protected int $length = 0;
​
    /**
     * @param mixed $calls
     */
    public function push(mixed $calls): void
    {
        $this->calls[] = $calls;
        ++$this->length;
    }
​
    public function next($param): void
    {
        ++$this->i;
        for (; $this->i < $this->length; ++$this->i) {
            $this->calls[$this->i]($param, [$this, 'next']);
        }
    }
    
     public function nextX($param)
    {
        ++$this->i;
        for (; $this->i < $this->length; ++$this->i) {
            return $this->calls[$this->i]($param, [$this, 'next']);//或nextX
        }
    }
     public function abort(): void
    {
        $this->i=count($this->calls)+1;
    }
}
​
$m = new middleware();
$middlewares = [[$aa, 'call'], [$bb, 'call'], [$cc, 'cc']];
foreach ($middlewares as $callable) {
    $m->push($callable);
}
$m->next(new p());
​

这种么是只用for循环实现,好理解得多,这里还有两种next的调用方式,用方法nextX可以完全像array_reduce那种严格的手动调用next闭包,可以有返回值。另一种是使用方法next,这种可以不用手动调用next闭包就可以调用下一个中间件,不过不能有返回值(返回值可以通过引用类型的参数传递),而且中断的话就必须得手动调用abort方法了,这样的话可能还得调整个中间件方法的参数,略麻烦些。但两种方式注册的中间件最后一个都需同array_reduce方式的$initial的函数原型一致或者最后一个不调用$next函数。

最后么感觉用array_reduce的方式其实是要方便得多,虽然那个构造闭包的function真的是绕得想撞墙的节奏就是了。是得要最先就得确定好最终或者说那个initial闭包的原型,然后记得中间件的那个next参数一定就是那个闭包后其实还好。哦,对了这个又叫做柯里化,不得不说不管哪种方式实现这种层层调用的人真可乃神人也。。。。。

谨以此文,致曾经面试自己的那位cto面试官及对代码有追求的人,其实当时自己只会用for的那种方式 🤣 🤣 🤣。。。。。。

发表回复

您的电子邮箱地址不会被公开。