Laravel 中,管道(Pipeline)组件是实现路由中间件而使用的重要工具之一。通过管道组件,可以通过执行一系列方法,从而对数据进行处理。

自制管道

在开始讲解 LaravelPipeline 之前,建议先动手实现一个管道组件,这样有利于理解 LaravelPipeline 源码。 我们自己实现的管道的 Interface 可定义如下:

1
2
3
4
5
6
7
interface Pipeline
{
    public function send($passable);
    // 定义管道中流动的数据,即被执行方法的参数
    public function through(array $pipelines);
    // 定义要通过哪些方法
}

foreach 版管道

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
<?php
class Pipeline
{
    protected $passable;

    public function send($passable)
    {
        $this->passable = $passable;

        return $this;
    }

    public function through(array $pipelines)
    {
        $passable = $this->passable;
        foreach ($pipelines as $pipeline) {
            $passable = $pipeline($passable);
        }

        return $passable;
    }
}

array_reduce 版管道

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
<?php
class Pipeline
{
    protected $passable;

    public function send($passable)
    {
        $this->passable = $passable;

        return $this;
    }

    public function through(array $pipelines)
    {
        $passable = $this->passable;
        return array_reduce($pipelines, function ($carry, $pipeline) use ($passable) {
            return is_null($carry) ? $pipeline($passable) : $pipeline($carry);
        });
    }
}

array_reduce 的首次 reduce 会出现 null 元素,因此要进行 null 的判断。

测试代码

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
$a = function ($passable) {
    print('a');
    return $passable + 1;
};
$b = function ($passable) {
    print('b');
    return $passable + 2;
};
$c = function ($passable) {
    print('c');
    return $passable + 4;
};

$result = (new Pipeline())->send(10)->through([$a, $b, $c]);
print($result);

以上两种管道实现都通俗易懂,因此不细说。他们的运行结果均为:

1
abc17

自制管道总结

从逻辑上来说,上面两种实现都是基于正序遍历的。即正序遍历 $pipelines,把 要处理的数据依次喂给每个 $pipeline

Laravel 管道源码解析

下面将根据 Laravel 5.7 的源码来讲解。 Laravel 的管道位于 Illuminate\Pipeline\Pipeline 命名空间中,该文件代码简化结构如下:

1
2
3
4
5
6
7
8
interface Pipeline
{
    public function send($passable);
    public function through($pipes);
    public function then(Closure $destination);
    protected function prepareDestination(Closure $destination);
    protected function carry();
}

这五个方法,能基本构成一个完整的管道,因此将从这几个方法来分析源码。至于用法,可参考相关文档。 send 方法传入要处理的对象;through 方法设置执行函数队列;then 方法设置最终方法并依次执行队列中的函数。 sendthrough 方法都较为简单,因此不细说。

1
2
3
4
5
6
7
8
public function then(Closure $destination)
{
    $pipeline = array_reduce(
        array_reverse($this->pipes), $this->carry(), $this->prepareDestination($destination)
    );

    return $pipeline($this->passable);
}

then 方法的核心是 array_reduce 函数,它在 Laravel 中对执行函数队列进行了翻转,使得先添加到队列的函数后执行,变成了栈的结构。prepareDestination 方法返回的闭包,充当 array_reduceinitial 参数,最先执行。

1
2
3
4
5
6
protected function prepareDestination(Closure $destination)
{
    return function ($passable) use ($destination) {
        return $destination($passable);
    };
}

prepareDestination 方法返回的是包含有最终执行方法的闭包($destination),其参数为要处理的对象($passable)。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
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)) {
                ···
        };
    };
} 

carry 方法提供给 then 方法中 array_reduce 函数 callback 参数的是一个参数为 $passable 、环境为 $stack, $pipe 的闭包。其中 $stack 为上次迭代的值,如果是第一次迭代,该值为 prepareDestination 方法返回的闭包;$pipe 为本次迭代的值。 可以看到,array_reduce 的每次 reduce 后,会将新、旧 reduce 元素封存在闭包中,并返回它。而执行该闭包返回的是 $pipe($passable, $stack)。 这样形成了栈的逻辑,而之前的 array_reduce 中,使用了 array_reverse ,因此两次取反得正,then 方法的 array_reduce 事实上是按照添加到队列的函数的先后顺序来执行的。

举个例子:

1
2
3
4
5
(new Pipeline($this->app))->send($passable)->through([
    $f1,
    $f2,
    $f3,
])->then($distination);

对应地,在 then 方法中,最终得到的 $pipeline

1
2
3
4
5
6
7
function($passable) {
    return $f1($passable, function($passable) {
        return $f2($passable, function($passable) {
            return $f3($passable, $destination());
        }); 
    });
};

这就实现了管道队列中函数的顺序执行。

对比

从逻辑上来说,自制组件是直接按顺序把数据传给每个方法;而 Laravel 是把待执行的任务逆序捣腾两遍,弄成正序的。

Laravel 的组件中,被执行的任务,必须要有表示下一个管道的参数(如 Laravel 路由中间件的 handle 方法的 $next 参数),并且在方法执行完后,执行 $next(···) 来执行下一个 pipe。 而自制的管道组件,直接返回处理完的对象就行,较 Laravel 方便一些。

参考文献

https://www.jianshu.com/p/fab65bc93896

https://www.jianshu.com/p/93cf2b233775