laravel 管道設計模式

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

      所謂管道(Pipeline)設計模式,就是把數據傳遞給一個任務隊列,由任務隊列按次序依次對數據進行加工處理。在laravel框架中,這裏的數據就是http請求,任務隊列包含了一個又一個的中間件。php

      類比1:以流水線或流水管道做類比,流水線上的產品(http請求),依次通過一個又一個的加工單元(對應一個又一個的中間件)進行處理,最後生成產品(http響應)。html

      類比2:一樣的,也能夠與linux下的管道做類比,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.phplaravel

 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

    接下來,是對管道的實現.git

protected $container;  //保存服務容器的實例github

protected $passable;  //保存傳入的http請求shell

protected $pipes = [];  //保存子任務隊列,子任務可使閉包函數,也可使類名與參數名的字符串組合設計模式

protected $method = 'handle';  //當子任務是類名+參數的字符串組合時,$method指定在管道處理到該類子任務時,該類子任務用來處理http請求的方法名。$method默認是handle,可是能夠經過                  via()方法修改閉包

在一次http的請求過程當中,如下方法的被調用的過程是:send()  ->   through()  ->  then()  。app

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. 不依賴於任何框架的管道

相關文章
相關標籤/搜索