Laravel 源码分析---Pineline

在 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 的源码后,其中 sendthroughviaparsePipeString 等方法非常容易理解,而 getSlicegetInitialSlice 这两个方法用了相对较多的闭包,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
num2
计数器初始值 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 实现中间件功能)。

参考文档
1. 理解Laravel中的pipeline

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
好的,我会尽力回答您关于 Jenkins 流水线(Pipeline)的问题。 1. 什么是 Jenkins Pipeline? Jenkins Pipeline 是一种插件,它允许用户使用 Groovy 脚本来定义整个构建过程。Pipeline 可以将整个构建过程划分为多个阶段(Stage),并且可以在每个阶段中执行多个步骤(Step)。Pipeline 还可以将构建过程与版本控制系统集成,以实现更好的可视化和持续交付。 2. 如何创建一个 Jenkins Pipeline? 首先,您需要安装 Pipeline 插件。然后,在 Jenkins 界面中,选择“新建任务”,然后选择“流水线”类型。接下来,您需要编写 Pipeline 脚本,该脚本将定义整个构建过程。一般来说,您可以将脚本存储在源代码控制系统中,并在 Jenkins 中指定该脚本的位置。 3. Jenkins Pipeline 的优势是什么? Pipeline 可以将整个构建过程可视化,并允许用户以更可读的方式管理构建。Pipeline 还可以将构建过程与版本控制系统集成,提高了代码的可追溯性和可维护性。Pipeline 还可以支持并行构建,从而提高了构建效率。 4. 如何调试 Jenkins Pipeline? 您可以使用 Jenkins Blue Ocean 插件来可视化 Pipeline 的执行过程。Blue Ocean 提供了一个交互式界面,允许您查看 Pipeline 的整个执行过程,并对执行过程进行调试。您还可以使用 Jenkins 的日志功能来查看 Pipeline 的详细输出信息,以便更好地了解 Pipeline 的执行过程。 希望这些信息对您有所帮助。如果您有任何其他问题,请随时告诉我。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值