Laravel 源码分析---Pineline

标签: 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 的源码后,其中 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
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 实现中间件功能)。

参考文档

  1. 理解Laravel中的pipeline
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 212,080评论 6 493
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,422评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 157,630评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,554评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,662评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,856评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,014评论 3 408
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,752评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,212评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,541评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,687评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,347评论 4 331
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,973评论 3 315
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,777评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,006评论 1 266
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,406评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,576评论 2 349

推荐阅读更多精彩内容