标签: laravel 源码分析 Pipeline
在 laravel 框架中,Illuminate\Pipeline\Pipeline
类是实现 laravel 中间件功能的重要工具之一。他的作用是,将一系列有序可执行的任务依次执行。也有人把这种功能成为管道模式,比如下面这篇文章的介绍:
Laravel 中管道设计模式的使用 —— 中间件实现原理探究
今天我们就来探究一下 Pipeline 类的功能和源码。
Pipeline 的使用
Pipeline(管道)顾名思义,就是将一系列任务按一定顺序在管道里面依次执行。其中任务可以是匿名函数,也可以是拥有特定方法的类或对象。
我看先来看一段 Pipeline 的使用代码,了解一下Pipeline 具体是如何使用的。
namespace App\Console\Commands;
use Illuminate\Console\Command;
use Illuminate\Pipeline\Pipeline;
class Test extends Command
{
/**
* The name and signature of the console command.
*
* @var string
*/
protected $signature = 'test';
/**
* Execute the console command.
*
* @return mixed
*/
public function handle()
{
$task1 = function($passable, $next){
$this->info('这是任务1');
$this->info('任务1的参数 '.$passable);
return $next($passable);
};
$task2 = function($passable, $next){
$this->info('这是任务2');
$this->info('任务2的参数 '.$passable);
return $next($passable);
};
$task3 = function($passable, $next){
$this->info('这是任务3');
$this->info('任务3的参数 '.$passable);
return $next($passable);
};
$pipeline = new Pipeline();
$rel = $pipeline->send('任务参数')
->through([$task1, $task2, $task3])
->then(function(){
$this->info('then 方法');
return 'then 方法的返回值';
});
$this->info($rel);
}
}
运行上面代码,我们得到如下结果
这是任务1
任务1的参数 任务参数
这是任务2
任务2的参数 任务参数
这是任务3
任务3的参数 任务参数
then 方法
then 方法的返回值
通过上面代码我们可以知道,Pipeline 中 through
方法设置要依次执行的任务,send
设置传入任务的参数,then
设置最终要执行的任务,并依次执行任务队列。
Pipeline 源码分析
在了解完 Pipeline 用法之后,我们先来大概看一下 Pipeline 的源码。
namespace Illuminate\Pipeline;
use Closure;
use RuntimeException;
use Illuminate\Contracts\Container\Container;
use Illuminate\Contracts\Pipeline\Pipeline as PipelineContract;
class Pipeline implements PipelineContract
{
/**
* The container implementation.
*
* @var \Illuminate\Contracts\Container\Container
*/
protected $container;
/**
* The object being passed through the pipeline.
* 传入 Pipeline 任务队列的参数
* @var mixed
*/
protected $passable;
/**
* The array of class pipes.
* 依次要执行的任务队列
* @var array
*/
protected $pipes = [];
/**
* The method to call on each pipe.
* 对于类或者对象表示的任务,执行任务要调用的方法
* @var string
*/
protected $method = 'handle';
/**
* Set the object being sent through the pipeline.
* 设置传入任务的参数
* @param mixed $passable
* @return $this
*/
public function send($passable)
{
$this->passable = $passable;
return $this;
}
/**
* Set the array of pipes.
* 设置任务队列
* @param array|mixed $pipes
* @return $this
*/
public function through($pipes)
{
$this->pipes = is_array($pipes) ? $pipes : func_get_args();
return $this;
}
/**
* Set the method to call on the pipes.
* 设置执行类任务或者对象任务的调用方法
* @param string $method
* @return $this
*/
public function via($method)
{
$this->method = $method;
return $this;
}
/**
* Run the pipeline with a final destination callback.
* 设置最终任务,依次执行任务队列
* @param \Closure $destination
* @return mixed
*/
public function then(Closure $destination)
{
$firstSlice = $this->getInitialSlice($destination);
$callable = array_reduce(
array_reverse($this->pipes), $this->getSlice(), $firstSlice
);
return $callable($this->passable);
}
/**
* Get a Closure that represents a slice of the application onion.
* 返回使用匿名函数包装任务并将其加入任务栈的匿名函数
* @return \Closure
*/
protected function getSlice()
{
$outFunc = function ($stack, $pipe) {
$innerFunc = function ($passable) use ($stack, $pipe) {
if ($pipe instanceof Closure) {
//如果要执行的任务 $pipe 是一个匿名函数的话,
//我们将立即执行这个匿名函数并返回其结果;
return $pipe($passable, $stack);
} elseif (! is_object($pipe)) {
//如果 $pipe 不是对象的话(为字符串),
//我们将从 $pipe 中解析出来任务名称和可能存在的参数
list($name, $parameters) = $this->parsePipeString($pipe);
//根据任务名称在容器中解析出来任务对象
$pipe = $this->getContainer()->make($name);
//构建任务执行所需的参数
$parameters = array_merge([$passable, $stack], $parameters);
} else {
//如果 $pipe 是一个对象的话,我们构建出任务执行所需的参数
$parameters = [$passable, $stack];
}
//调用任务对象并返回其结果
return $pipe->{$this->method}(...$parameters);
};
return $innerFunc;
};
return $outFunc;
}
/**
* Get the initial slice to begin the stack call.
* 对任务 $destination 使用匿名函数进行包装
* @param \Closure $destination
* @return \Closure
*/
protected function getInitialSlice(Closure $destination)
{
return function ($passable) use ($destination) {
return $destination($passable);
};
}
/**
* Parse full pipe string to get name and parameters.
* 根据 $pipe 解析出任务名称和传入任务的额外参数(如果存在的话)
* 比如中间件 throttle:60,1 的设置,
* 解析出任务名称 throttle,参数 [60,1]
* @param string $pipe
* @return array
*/
protected function parsePipeString($pipe)
{
list($name, $parameters) = array_pad(explode(':', $pipe, 2), 2, []);
if (is_string($parameters)) {
$parameters = explode(',', $parameters);
}
return [$name, $parameters];
}
}
看完 Pipeline 的源码后,其中 send
、through
、via
、parsePipeString
等方法非常容易理解,而 getSlice
、getInitialSlice
这两个方法用了相对较多的闭包,then
方法是最终的调用方法,这三个方法相对较难理解。下面我们通过文章开头的例子来看这三个方法具体是如何执行的。
首先让我们来看一下 PHP 中闭包的特性
PHP 中的闭包
首先,我们来通过一个计数器的例子,来看一下 PHP 中闭包的使用。
$num = 1;
$count = function()use($num){ //$num 没有引用符 &
$this->info('计数器初始值 '.$num);
return function()use(&$num){ //$num 有引用符 &
$num++;
return $num;
};
};
$counter1 = $count();
$this->info('计数器值: '.$counter1());
$this->info('计数器值: '.$counter1());
$this->info('计数器值: '.$counter1());
$num++;
$this->info('num 值'.$num);
$counter2 = $count();
$this->info('计数器值: '.$counter2());
$this->info('计数器值: '.$counter2());
$this->info('计数器值: '.$counter2());
首先,我们定义了一个计数器创建函数 $count
,每次调用这个函数都会创建一个计数器并返回,并且在创建计数器时使用了外部变量 $num
。然后我们在 $num
值为 1 的时候创建了计数器 $counter1
,在 $num
值为 2 的时候创建了计数器 $counter2
,并分别计数。
注:在 $count
函数定义的时候 use( $num )
的时候没有引用符 &
,在函数里面返回计数器时,use( &$num )
,使用了引用符 &
,想想为什么。
运行上面代码,我们得到下面结果:
计数器初始值 1
计数器值: 2
计数器值: 3
计数器值: 4
num 值2
计数器初始值 1
计数器值: 2
计数器值: 3
计数器值: 4
通过上面代码我们知道,在 PHP 的匿名函数 use
外部变量的时候,如果有引用符 &
,代码就会取变量的引用,函数里面对引用变量的修改也会影响外部变量;如果没有引用符,代码就会重新分配一个变量并存储在函数的调用栈里面,在函数里面对引用变量的修改,并不会改变外部变量的值。
了解完 PHP 闭包的特性后,我们来看一下 Pipeline 核心源码的执行过程。
Pipeline 核心代码分析
我们结合文章开头的例子来分析 Pipeline 中 then
方法的具体执行过程。
我们先来看 then
方法的代码:
/**
* Run the pipeline with a final destination callback.
* 设置最终任务,依次执行任务队列
* @param \Closure $destination
* @return mixed
*/
public function then(Closure $destination)
{
$firstSlice = $this->getInitialSlice($destination);
$callable = array_reduce(
array_reverse($this->pipes), $this->getSlice(), $firstSlice
);
return $callable($this->passable);
}
在这里面 $this->pipes
,值为 [$task1,$task2,$task3]
,表示任务队列;$destination
表示最终任务。
当执行 $firstSlice = $this->getInitialSlice($destination)
,我们得到 $firstSlice
变量如下:
$firstSlice = function ($passable) use ($destination) {
return $destination($passable);
};
执行第二行代码,得到的 $callable
变量是 Pileline 代码的核心。这行代码主要是以 $firstSlice
为初始值,使用方法 $this->getSlice()
作为回调将数组 $this->pipes
的反转数组 [$task3,$task2,$task1]
里面的元素依次合并得到单一的依次存储有各个任务匿名函数,并将其返回给 $callable
变量。(array_reduce 用回调函数迭代地将数组简化为单一的值)
我们先来看针对 $task3
和 $firstSlice
的使用 $this->getSlice
的合并情况。
我们再来复习一下 getSlice
的源码:
/**
* Get a Closure that represents a slice of the application onion.
* 返回使用匿名函数包装任务并加入任务栈的匿名函数
* @return \Closure
*/
protected function getSlice()
{
$outFunc = function ($stack, $pipe) {
$innerFun = function ($passable) use ($stack, $pipe) {
if ($pipe instanceof Closure) {
//如果要执行的任务 $pipe 是一个匿名函数的话,
//我们将立即执行这个匿名函数并返回其结果;
return $pipe($passable, $stack);
} elseif (! is_object($pipe)) {
//如果 $pipe 不是对象的话(为字符串),
//我们将从 $pipe 中解析出来任务名称和可能存在的参数
list($name, $parameters) = $this->parsePipeString($pipe);
//根据任务名称在容器中解析出来任务对象
$pipe = $this->getContainer()->make($name);
//构建任务执行需的参数
$parameters = array_merge([$passable, $stack], $parameters);
} else {
//如果 $pipe 是一个对象的话,我们构建出任务执行所需的参数
$parameters = [$passable, $stack];
}
//调用任务对象并返回其结果
return $pipe->{$this->method}(...$parameters);
};
return $innerFun;
};
return $outFunc;
}
在使用 $this->getSlice
对 $task3
和 $firstSlice
进行合并,实力上就是运行$this->getSlice
中的 $outFunc
函数,其中
$stack = $firstSlice;
$pipe = $task1;
运行 $this->getSlice
中的 $outFunc
方法返回变量 $innerFun
(其为合并 $task3
和 $firstSlice
后的匿名函数,设为 $stack1
)。其中 $task3
和 $firstSlice
分别作为 $pipe
和 $stack
变量的的值,存储在匿名函数 $stack1
中。
接下来合并 $task2
,运行 $this->getSlice
中的 $outFunc
方法,得到匿名函数 $stack2
,其中 $task2
和 $stack1
分别作为 $pipe
和 $stack
变量的的值,存储在匿名函数 $stack2
中。
最后合并 $task1
,运行 $this->getSlice
中的 $outFunc
方法,得到匿名函数 $stack3
,其中 $task1
和 $stack2
分别作为 $pipe
和 $stack
变量的的值,存储在匿名函数 $stack3
中。
最后 $stack3
返回给 $callable
,$callable
是一个匿名函数,调用 $callable
会依次递归调用队列里的任务。
创建依次递归执行任务队列的匿名函数主要是通过 array_reduce
函数使用 $this->getSlice
作用回调函数,以 $firstSlice
为初始值,对任务队列反向迭代合并得到的。在每次迭代合并的过程中,要执行的任务和旧的任务栈都会作为新的任务栈(本质为匿名函数)的 use
变量存在新的任务栈(匿名函数)中。
总结
至此,我们分析完了 Pipeline 的源码以及其执行过程,在 laravel 框架中,Pipeline 的主要作用是实现框架中间件的功能。以后我们将会看这部分相应的源码(见文章Laravel 源码分析---使用 Pipeline 实现中间件功能)。
参考文档
- 理解Laravel中的pipeline