随笔 - 38  文章 - 0 评论 - 0 trackbacks - 0

  1. laravel中的管道(Pipeline)是什么?

      所谓管道(Pipeline)设计模式,就是把数据传递给一个任务队列,由任务队列按次序依次对数据进行加工处理。在laravel框架中,这里的数据就是http请求,任务队列包含了一个又一个的中间件。

      类比1:以流水线或流水管道作类比,流水线上的产品(http请求),依次经过一个又一个的加工单元(对应一个又一个的中间件)进行处理,最后生成产品(http响应)。

      类比2:同样的,也可以与linux下的管道作类比,

    cat helloworld.txt | grep "hello world" | rev | > output.txt
      不过,差异的地方是,linux shell管道中任务队列中的单元是一个又一个的进程。而laravel框架中的Pipeline是运行在一个进程中的一个又一个的程序块,或者说逻辑片。
  2. laravel中如何使用pipeline?
      Laravel 在框架中的很多地方使用了管道设计模式,最常见的就是中间件的实现。
      当请求最终到达控制器动作被处理前,会先经过一系列的中间件。每个中间价都有一个独立的职责,例如,设置 Cookie、判断是否登录以及阻止 CSRF 攻击等等。每个阶段都会对请求进行处理,如果请求通过就会被传递给下一个处理,不通过就会返回相应的 HTTP 响应。
      这种机制使得我们很容易在请求最终到达应用代码前添加处理操作,当然如果不需要这个处理操作你也可以随时移除而不影响请求的生命周期。
  3. Pipeline有什么优点?
    1. 将复杂的处理流程分解成独立的子任务,从而方便测试每个子任务;
    2. 被分解的子任务可以被不同的处理进程复用,避免代码冗余。(这里说的不同的处理进程是指,针对不同的http请求,采用不同的子任务组合来处理)
    3. 在复杂进程中添加、移除和替换子任务非常轻松,对已存在的进程没有任何影响。
  4. Pipeline有什么缺点?
    1. 虽然每个子任务变得简单了,但是当你再度尝试将这些子任务组合成完整进程时有一定复杂性;
    2. 你还需要保证独立子任务测试通过后整体的流程能正常工作,这有一定的不确定性。(因为在管道中流动的是http请求,并且子任务可以修改http请求,这样就存在前一次的修改内容导致下一个子任务的执行失败的可能性)
    3. 当你看到的都是一个个子任务时,对理解整体流程带来困难(盲人摸象的故事想必大家很熟悉,正是此理)。
  5. 代码理解
    这里只局部分析管道实现的三个文件,它们并没有组成一个完整的工作流程。想要了解完整流程,还需要研究后面的几个文件。
    分析的文件:
    1.  laravel/vendor/laravel/framework/src/Illuminate/Contracts/Pipeline/Pipeline.php
    2.  laravel/vendor/laravel/framework/src/Illuminate/Pipeline/Pipeline.php
    3.  laravel/vendor/laravel/framework/src/Illuminate/Routing/Pipeline.php

    了解完整流程,还需要看以下文件:
    1.  laravel/public/index.php
    2.  laravel/vendor/laravel/framework/src/Illuminate/Foundation/Http/Kernel.php
    3.  laravel/vendor/laravel/framework/src/Illuminate/Foundation/Application.php
    下面的部分是对Pipeline接口的定义,定义了send、through、via、then四个方法。(这里不知道via方法具体是干嘛的)

laravel\vendor/laravel/framework/src/Illuminate/Contracts/Pipeline/Pipeline.php

 1 <?php
 2 
 3 namespace Illuminate\Contracts\Pipeline;
 4 
 5 use Closure;
 6 
 7 interface Pipeline
 8 {
 9     /**
10      * Set the traveler object being sent on the pipeline.
11      *
12      * @param  mixed  $traveler
13      * @return $this
14      */
15     public function send($traveler);
16 
17     /**
18      * Set the stops of the pipeline.
19      *
20      * @param  dynamic|array  $stops
21      * @return $this
22      */
23     public function through($stops);
24 
25     /**
26      * Set the method to call on the stops.
27      *
28      * @param  string  $method
29      * @return $this
30      */
31     public function via($method);
32 
33     /**
34      * Run the pipeline with a final destination callback.
35      *
36      * @param  \Closure  $destination
37      * @return mixed
38      */
39     public function then(Closure $destination);
40 }
View Code

    接下来,是对管道的实现.

protected $container;  //保存服务容器的实例

protected $passable;  //保存传入的http请求

protected $pipes = [];  //保存子任务队列,子任务可以使闭包函数,也可以使类名与参数名的字符串组合

protected $method = 'handle';  //当子任务是类名+参数的字符串组合时,$method指定在管道处理到该类子任务时,该类子任务用来处理http请求的方法名。$method默认是handle,但是可以通过                  via()方法修改

在一次http的请求过程中,以下方法的被调用的过程是:send()  ->   through()  ->  then()  。

public function send($passable){}  //传入初始的http请求

public function through($pipes){}  //设置管道的子任务队列

public function via($method){}  //设置$method的值

public function then(Closure $destination){}  //启动管道,用设定的子任务队列去处理http请求

protected function getSlice(){}      //本函数返回array_reduce()中所需要的第二个参数,callback函数

protected function getInitialSlice(Closure $destination){}    //本函数对应array_reduce()中所需要的第三个参数,初始化值

protected function parsePipeString($pipe){}    //针对子任务是类名+参数名的字符串组合,提取类名和参数

 由上面可知,整个管道的处理逻辑主要集中在then()方法中。

then()方法中,最难懂的是下面这句话:

1         return call_user_func(
2             array_reduce($pipes, $this->getSlice(), $firstSlice), $this->passable
3         );

 

结合getSlice()方法,array_reduce()的处理过程实际上是,利用了闭包函数的特点,用闭包函数保存了局部作用域中的参数$stack和$pipe,并将保存了局部scope的闭包函数作为对象,压如由$stack保存的堆栈中,当将整个逆序的子任务队列的执行函数的闭包函数形式压入栈中后,再通过call\_user\_func(),传入$this->passable(即http请求),从栈中依次弹出闭包函数处理请求。

 

laravel\vendor/laravel/framework/src/Illuminate/Pipeline/Pipeline.php

  1 <?php
  2 
  3 namespace Illuminate\Pipeline;
  4 
  5 use Closure;
  6 use Illuminate\Contracts\Container\Container;
  7 use Illuminate\Contracts\Pipeline\Pipeline as PipelineContract;
  8 
  9 class Pipeline implements PipelineContract
 10 {
 11     /**
 12      * The container implementation.
 13      *
 14      * @var \Illuminate\Contracts\Container\Container
 15      */
 16     protected $container;
 17 
 18     /**
 19      * The object being passed through the pipeline.
 20      *
 21      * @var mixed
 22      */
 23     protected $passable;
 24 
 25     /**
 26      * The array of class pipes.
 27      *
 28      * @var array
 29      */
 30     protected $pipes = [];
 31 
 32     /**
 33      * The method to call on each pipe.
 34      *
 35      * @var string
 36      */
 37     protected $method = 'handle';
 38 
 39     /**
 40      * Create a new class instance.
 41      *
 42      * @param  \Illuminate\Contracts\Container\Container  $container
 43      * @return void
 44      */
 45     public function __construct(Container $container)
 46     {
 47         $this->container = $container;
 48     }
 49 
 50     /**
 51      * Set the object being sent through the pipeline.
 52      *
 53      * @param  mixed  $passable
 54      * @return $this
 55      */
 56     public function send($passable)
 57     {
 58         $this->passable = $passable;
 59 
 60         return $this;
 61     }
 62 
 63     /**
 64      * Set the array of pipes.
 65      *
 66      * @param  array|mixed  $pipes
 67      * @return $this
 68      */
 69     public function through($pipes)
 70     {
 71         $this->pipes = is_array($pipes) ? $pipes : func_get_args();
 72 
 73         return $this;
 74     }
 75 
 76     /**
 77      * Set the method to call on the pipes.
 78      *
 79      * @param  string  $method
 80      * @return $this
 81      */
 82     public function via($method)
 83     {
 84         $this->method = $method;
 85 
 86         return $this;
 87     }
 88 
 89     /**
 90      * Run the pipeline with a final destination callback.
 91      *
 92      * @param  \Closure  $destination
 93      * @return mixed
 94      */
 95     public function then(Closure $destination)
 96     {
 97         $firstSlice = $this->getInitialSlice($destination);
 98 
 99         $pipes = array_reverse($this->pipes);
100 
101         return call_user_func(
102             array_reduce($pipes, $this->getSlice(), $firstSlice), $this->passable
103         );
104     }
105 
106     /**
107      * Get a Closure that represents a slice of the application onion.
108      *
109      * @return \Closure
110      */
111     protected function getSlice()
112     {
113         return function ($stack, $pipe) {
114             return function ($passable) use ($stack, $pipe) {
115                 // If the pipe is an instance of a Closure, we will just call it directly but
116                 // otherwise we'll resolve the pipes out of the container and call it with
117                 // the appropriate method and arguments, returning the results back out.
118                 if ($pipe instanceof Closure) {
119                     return call_user_func($pipe, $passable, $stack);
120                 } else {
121                     list($name, $parameters) = $this->parsePipeString($pipe);
122 
123                     return call_user_func_array([$this->container->make($name), $this->method],
124                             array_merge([$passable, $stack], $parameters));
125                 }
126             };
127         };
128     }
129 
130     /**
131      * Get the initial slice to begin the stack call.
132      *
133      * @param  \Closure  $destination
134      * @return \Closure
135      */
136     protected function getInitialSlice(Closure $destination)
137     {
138         return function ($passable) use ($destination) {
139             return call_user_func($destination, $passable);
140         };
141     }
142 
143     /**
144      * Parse full pipe string to get name and parameters.
145      *
146      * @param  string $pipe
147      * @return array
148      */
149     protected function parsePipeString($pipe)
150     {
151         list($name, $parameters) = array_pad(explode(':', $pipe, 2), 2, []);
152 
153         if (is_string($parameters)) {
154             $parameters = explode(',', $parameters);
155         }
156 
157         return [$name, $parameters];
158     }
159 }
View Code

 下面是对管道添加了异常处理的实现。

laravel\vendor/laravel/framework/src/Illuminate/Routing/Pipeline.php

 1 <?php
 2 
 3 namespace Illuminate\Routing;
 4 
 5 use Closure;
 6 use Throwable;
 7 use Exception;
 8 use Illuminate\Http\Request;
 9 use Illuminate\Contracts\Debug\ExceptionHandler;
10 use Illuminate\Pipeline\Pipeline as BasePipeline;
11 use Symfony\Component\Debug\Exception\FatalThrowableError;
12 
13 /**
14  * This extended pipeline catches any exceptions that occur during each slice.
15  *
16  * The exceptions are converted to HTTP responses for proper middleware handling.
17  */
18 class Pipeline extends BasePipeline
19 {
20     /**
21      * Get a Closure that represents a slice of the application onion.
22      *
23      * @return \Closure
24      */
25     protected function getSlice()
26     {
27         return function ($stack, $pipe) {
28             return function ($passable) use ($stack, $pipe) {
29                 try {
30                     $slice = parent::getSlice();
31 
32                     return call_user_func($slice($stack, $pipe), $passable);
33                 } catch (Exception $e) {
34                     return $this->handleException($passable, $e);
35                 } catch (Throwable $e) {
36                     return $this->handleException($passable, new FatalThrowableError($e));
37                 }
38             };
39         };
40     }
41 
42     /**
43      * Get the initial slice to begin the stack call.
44      *
45      * @param  \Closure  $destination
46      * @return \Closure
47      */
48     protected function getInitialSlice(Closure $destination)
49     {
50         return function ($passable) use ($destination) {
51             try {
52                 return call_user_func($destination, $passable);
53             } catch (Exception $e) {
54                 return $this->handleException($passable, $e);
55             } catch (Throwable $e) {
56                 return $this->handleException($passable, new FatalThrowableError($e));
57             }
58         };
59     }
60 
61     /**
62      * Handle the given exception.
63      *
64      * @param  mixed  $passable
65      * @param  \Exception  $e
66      * @return mixed
67      *
68      * @throws \Exception
69      */
70     protected function handleException($passable, Exception $e)
71     {
72         if (! $this->container->bound(ExceptionHandler::class) || ! $passable instanceof Request) {
73             throw $e;
74         }
75 
76         $handler = $this->container->make(ExceptionHandler::class);
77 
78         $handler->report($e);
79 
80         $response = $handler->render($passable, $e);
81 
82         if (method_exists($response, 'withException')) {
83             $response->withException($e);
84         }
85 
86         return $response;
87     }
88 }
View Code

 参考文献:

1. Laravel 中管道设计模式的使用 —— 中间件实现原理探究

2. 不依赖于任何框架的管道

posted on 2017-04-27 16:47 jade640 阅读(...) 评论(...) 编辑 收藏