Laravel 中间件实现原理

本文涉及的产品
云原生网关 MSE Higress,422元/月
Serverless 应用引擎免费试用套餐包,4320000 CU,有效期3个月
注册配置 MSE Nacos/ZooKeeper,118元/月
简介: Laravel 中间件实现原理

故事的开始


先不着急着分析源码 ps: 前戏得足 我们需要模拟出一个场景带入。假设我们现在需要计算这样的一个值,后面的结果都依赖于前面计算的值,现在我就是要通过中间件去实现这个功能。


1668568627968.jpg


为了减少计算量,这里我们主要看 E 这个计算公式部分,我们把整个 E 也看成 4 个部分,1 + 增值税税率 (即 count1),1/count1 (即 count2),count2 * 核定应税所得率 (即 count3) 以及 count3 * 倒算适用表税率 (即 count4).


场景出来了,我们创建四个类,分别代表 4 个中间件,count1……, 这里我们再强行制造一个场景,我们在计算 count3 的 时候,需要依赖于 count4 的计算结果,但是由于某些不可描述的约束,count3 又必须在 count4 之前进行一些条件的验证,也就是说 count3 的中间件走在 count4 的前面,但是计算却要依赖 count4 的结果处理。然后我们创建了四个类,把上面的四个步骤计算分别放入到这四个类中,我们也和平时使用中间件一样,增加一个 handle() 方法。

class Count1
{
    public function handle($value, \Closure $next)
    {
        $value += 1;
        var_dump('count1计算出来的值:' . $value);
        if ($value < 0) {
            return "系统异常";
        }
        return $next($value);
    }
}
class Count2
{
    public function handle($value, \Closure $next)
    {
        $value = 1 / $value;
        var_dump("count2 计算出来的值:$value") . PHP_EOL;
        return $next($value);
    }
}
class Count3
{
    //核定应税所得率依赖于后面的结果
    public function handle($value, \Closure $next)
    {
        $res = $next($value);
        var_dump("核定应税所得率收到的结果:$res") . PHP_EOL;
        return $res* 0.5;
    }
}
class Count4
{
    //倒算税率值0.5
    public function handle($value, \Closure $next)
    {
        $value *= 0.5;
        var_dump("倒算税率值计算后的值:$value") . PHP_EOL;
        return $next($value);
    }
}

Pipeline 组件


Laravel 中间件处理的核心就是 Illuminate\Pipeline 这个组件类,在了解这个类之前,我们先看看中间件在 Larave 中扮演的角色。

1668568659341.jpg

在到达 Application 之前的中间件是前置中间件,比如一些验证 csrf, 判断是否登录,是否有权限。在 Application 之后的是后置中间件,就是格外的处理一些逻辑,比如给响应添加请求头,cookie……。


你看上面的类格式是不是很像你们平常写的中间件代码。唯一不同的是,我们现在不走路由或者其他方式走中间件,而是直接把底层处理中间件的 Pipeline 类 (一般说管道) 拿出来,执行代码。我们运行实验类的 test 方法

public function test()
    {
        $pipes = [(new Count1), (new Count2), (new Count3), (new Count4)];
        $this->dispatcher(1, $pipes);
    }
   //count4 算完之后的处理 处理结果返回到count3
    public function additionalHandle()
    {
        return function ($value) {
            return $value < 0.1 ? $value : $value * 0.5;
        };
    }
    public function dispatcher($value, $pipes)
    {
        $result = (new Pipeline)->send($value)->through($pipes)->then($this->additionalHandle());
        echo "最终结果: " . $result;
    }

1668568675864.jpg

这个流程中,先实例化了 Pipeline 类,调用 send 方法,传入我们初始化的值,也就是 1,赋值给变量 passable, 返回本身,继续调用 through, 定义的四个中间件类赋值给变量 pipes,然后继续调用 then,该方法需要传入一个闭包函数。这个闭包函数就是上面的 additionalHandle 函数返回的闭包。我们再来看结果,先经过前置中间件 count 1,2,4, 到达另类的 Application,到达这里后,我们开始响应了,把结果返回给后置中间键 count3 进行计算,count3 前面已经没有其他的后置处理了,最终由 count3 返回最终的结果。最终结果:


1668568685309.jpg

有注意到在 count1 中的一个判断吗

1668568692735.jpg

我们现在传入 -2 ,然后打印结果

1668568701675.jpg

这样的话,不满足条件,就再也进不了下一层 (前置) 中间件了。


我们可以开始分析源码,试着去找出 Laravel 是如何优雅的实现这个流程的。直接查看

then
public function then(Closure $destination)
    {
        //$destination 就是我们传入的additionalHandle返回的闭包
//        return function ($value) {
//            return $value < 0.1 ? $value : $value * 0.5;
//        };
       // $this->>pipes  就是传入中间件count1 2 3 4
        $pipeline = array_reduce(
            array_reverse($this->pipes), $this->carry(), $this->prepareDestination($destination)
        );
        //$this->passable 就是$value值,初始的时候传入的是1
        return $pipeline($this->passable);
    }

处理的核心就是在这行代码里面了,在继续分析代码之前,我们有必要了解一下 array_reduce 这个函数。可以说,Laravel 中的中间件完全依赖于这个函数。


array-reduce 函数


本来想想还是直接用一句:这个函数具体使用去查看文档,问了就是查文档。但是因为这个函数的重要性,还是写个没有 变异的案例吧。

1668568753321.jpg

假如我们需要把数组里的数求和

$data = [1, 2, 3, 4];
$res = array_reduce($data, function ($carry, $item) {
    return $carry + $item;
});
var_dump($res).PHP_EOL;
//有初始值5,那么传入第三个参数
$res = array_reduce($data, function ($carry, $item) {
    return $carry + $item;
},5);
var_dump($res);

1668568770146.jpg

够简单了吧。稍微了解一下 array_reduce 函数我们可以往下走了。我们重新回到之前的这段代码:

$pipeline = array_reduce(
            array_reverse($this->pipes), $this->carry(), $this->prepareDestination($destination)
        );

现在你已经知道三个参数在函数中的含义了。 array_reverse 会将我们定义的四个中间件全部通过 carry 函数处理 。我们来看看 carry 方法里面是啥。

/**
     * Get a Closure that represents a slice of the application onion.
     *
     * @return \Closure
     */
    protected function carry()
    {
        return function ($stack, $pipe) {
            return function ($passable) use ($stack, $pipe) {
                if (is_callable($pipe)) {
                    // If the pipe is an instance of a Closure, we will just call it directly but
                    // otherwise we'll resolve the pipes out of the container and call it with
                    // the appropriate method and arguments, returning the results back out.
                    return $pipe($passable, $stack);
                } elseif (!is_object($pipe)) {
                    list($name, $parameters) = $this->parsePipeString($pipe);
                    // If the pipe is a string we will parse the string and resolve the class out
                    // of the dependency injection container. We can then build a callable and
                    // execute the pipe function giving in the parameters that are required.
                    $pipe = $this->getContainer()->make($name);
                    $parameters = array_merge([$passable, $stack], $parameters);
                } else {
                    // If the pipe is already an object we'll just make a callable and pass it to
                    // the pipe as-is. There is no need to do any extra parsing and formatting
                    // since the object we're given was already a fully instantiated object.
                    $parameters = [$passable, $stack];
                }
                return method_exists($pipe, $this->method)
                    ? $pipe->{$this->method}(...$parameters)
                    : $pipe(...$parameters);
            };
        };
    }

可以看到,最终运行返回的是一个闭包函数,引用了外部的两个变量,stack 和 pipe。其中 $stack 第一次执行的时候,它的值就是定义的 additionalHandle 方法中返回的闭包函数 (注意:此时并不是真的执行闭包函数里面的逻辑)。第二次执行的时候就是第一次执行返回的闭包函数,第三次执行的时候….. 以此类推。


至于 pipe 则是每次迭代时的中间件类,第一次就是上面定义的 count4 ? 为什么不是 count 1, 因为使用了 array_reverse 函数。有人会说,这个 carry 好复杂啊,迭代的时候我的脑子想下面的运行逻辑根本就想不下去。其实,在执行 array_reverse 函数的时候,你完全不用考虑下面的逻辑🙅‍♂️,每次迭代的时候仅仅只是返回一个变量,只是这个变量有点特殊,是个闭包函数。并不是直接执行里面的逻辑,等到真正调用这个变量 (闭包函数) 的时候才执行代码。我们来看一个简单的不能再简单的例子:

$res = function ($item) {
    return $item * 10;
};

这个变量的值会是 2 吗?不会。它的值就是一个闭包函数,引用了一个外部的变量。既然值是闭包函数,那我可以调用吧:


$res(3)

这时候才是真正执行闭包函数的时候,传入了一个环境变量 3,执行闭包函数的逻辑,得到结果。

好了,现在我们可以知道 array_reverse 函数运行的整个流程。


第一下迭代:$stack 就是我们定义的 additionalHandle 闭包函数。pipe 就是第一个中间件 count4,

第二次迭代: $stack 是第一次执行返回的闭包函数,pipe 是 count3 中间件

第三次迭代: $stack 是第二次执行返回的闭包函数,pipe 是 count2 中间件

第四次迭代: $stack 是第三次执行返回的闭包函数,pipe 是 count1 中间件


现在迭代完毕,即 array_reverse 函数执行完毕,结果就是得到第四次迭代返回的闭包。然后运行这个闭包:

//$this->passable 就是$value值,还记得我们上面传入的是1
//$this->dispatcher(1, $pipes);
   return $pipeline($this->passable);

此时当前闭包 use 的就是 count1 这个类,然后我们可以开始看闭包里面的逻辑了,有三个分支

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];
                }

对于我们现在的场景来说,走的是 else 分支。把初始值以及上一次迭代返回的闭包都存入 parameters 数组中。 为什么是走 else 分支,还记得我们这个示例是咋么操作的嘛:

$pipes = [(new Count1), (new Count2), (new Count3), (new Count4)];
        $this->dispatcher(1, $pipes);

此时的 pipe 已然是个对象不需要去解析,前面的分支都和他没关系。继续往下走。

return method_exists($pipe, $this->method)
                    ? $pipe->{$this->method}(...$parameters)
                    : $pipe(...$parameters);
/**
    protected $method = 'handle';

现在知道为什么你的中间件默认都需要 handle 方法了吧。查看类中的 handle 方法。

public function handle($value, \Closure $next)
    {
        $value += 1;
        var_dump('count1计算出来的值:' . $value);
        if ($value < 0) {
            return "系统异常";
        }
        return $next($value);
    }

现在是到了第一个中间件核心处理的地方了,先简单的对变量进行处理,然后增加了过滤条件,如果不符合,直接打回去,这样就进不了下一层中间件了。也就是一开始说的那个 demo。如果要进入下一个中间件,那么就必须执行:

return $next($value);

为什么?因为上面说的 parameters 变量除了存储初始值之外,还存储了上一次迭代返回的闭包函数,此时你想进入下一层中间件,那么就必然要执行这个闭包函数,那为什么要 return ,这就是中间件的模型了,前置中间件处理完之后,到达 Application 然后开始执行后置中间件,一层层返回了。假设我们把 count 4 的 return 去掉,你应该能理解了吧。

class Count4
{
    //倒算税率值0.5
    public function handle($value, \Closure $next)
    {
        $value *= 0.5;
        var_dump("count4 倒算税率值计算后的值:$value") . PHP_EOL;
         $next($value);
    }
}

1668568788019.jpg

写到这里,也就分析的差不多了,一个小小的 array_reverse 函数,竟能玩出花。不得不佩服底层设计的巧妙。


结尾


最后,我很赞同 chongyi 在他的那篇文章说的一句话:要知道,再强大的 PHP 框架都是用 PHP 写出来的,本质上依旧是在一个大的基础上构建小型世界。

相关文章
|
4月前
|
中间件 PHP 开发者
深入解析 Laravel 中的 HTTP 中间件
【8月更文挑战第31天】
39 0
|
7月前
|
存储 分布式计算 Dubbo
【中间件】zookeeper的实现原理
【中间件】zookeeper的实现原理
100 0
|
消息中间件 负载均衡 中间件
【Alibaba中间件技术系列】「RocketMQ技术专题」让我们一起探索一下DefaultMQPullConsumer的实现原理及源码分析
【Alibaba中间件技术系列】「RocketMQ技术专题」让我们一起探索一下DefaultMQPullConsumer的实现原理及源码分析
199 3
【Alibaba中间件技术系列】「RocketMQ技术专题」让我们一起探索一下DefaultMQPullConsumer的实现原理及源码分析
|
安全 前端开发 中间件
PHP:laravel中间件和控制器的请求参数传递与获取
PHP:laravel中间件和控制器的请求参数传递与获取
272 10
PHP:laravel中间件和控制器的请求参数传递与获取
|
监控 JavaScript 前端开发
说说你对redux中间件的理解?常用的中间件有哪些?实现原理?
说说你对redux中间件的理解?常用的中间件有哪些?实现原理?
108 0
|
JavaScript 前端开发 中间件
对Redux中间件的理解?常用的中间件有哪些?实现原理?
对Redux中间件的理解?常用的中间件有哪些?实现原理?
127 0
|
中间件 数据安全/隐私保护
laravel-中间件
laravel-中间件
|
消息中间件 存储 负载均衡
【Alibaba中间件技术系列】「RocketMQ技术专题」让我们一起探索一下DefaultMQPushConsumer的实现原理及源码分析
【Alibaba中间件技术系列】「RocketMQ技术专题」让我们一起探索一下DefaultMQPushConsumer的实现原理及源码分析
204 5
|
中间件 PHP
【laravel】中间件
【laravel】中间件
136 6
【laravel】中间件
|
JSON 前端开发 NoSQL
Laravel表单篇-Request、Session、Response、Middleware
Laravel表单篇-Request、Session、Response、Middleware
151 0
Laravel表单篇-Request、Session、Response、Middleware
下一篇
DataWorks