在 Laravel 中,管道(Pipeline)组件是实现路由中间件而使用的重要工具之一。通过管道组件,可以通过执行一系列方法,从而对数据进行处理。
自制管道
在开始讲解 Laravel 的 Pipeline 之前,建议先动手实现一个管道组件,这样有利于理解 Laravel 的 Pipeline 源码。
我们自己实现的管道的 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);
|
以上两种管道实现都通俗易懂,因此不细说。他们的运行结果均为:
自制管道总结
从逻辑上来说,上面两种实现都是基于正序遍历的。即正序遍历 $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 方法设置最终方法并依次执行队列中的函数。
send、through 方法都较为简单,因此不细说。
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_reduce 的 initial 参数,最先执行。
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