在 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