美文网首页laravel 源码分析
Laravel 源码分析---Pineline

Laravel 源码分析---Pineline

作者: 上善若水_f6a4 | 来源:发表于2018-05-12 13:04 被阅读0次

    标签: 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

    相关文章

      网友评论

        本文标题:Laravel 源码分析---Pineline

        本文链接:https://www.haomeiwen.com/subject/lbncdftx.html