所謂管道(Pipeline)設計模式,就是把數據傳遞給一個任務隊列,由任務隊列按次序依次對數據進行加工處理。在laravel框架中,這裏的數據就是http請求,任務隊列包含了一個又一個的中間件。php
類比1:以流水線或流水管道做類比,流水線上的產品(http請求),依次通過一個又一個的加工單元(對應一個又一個的中間件)進行處理,最後生成產品(http響應)。html
類比2:一樣的,也能夠與linux下的管道做類比,linux
不過,差別的地方是,linux shell管道中任務隊列中的單元是一個又一個的進程。而laravel框架中的Pipeline是運行在一個進程中的一個又一個的程序塊,或者說邏輯片。cat helloworld.txt | grep "hello world" | rev | > output.txt
1. 將複雜的處理流程分解成獨立的子任務,從而方便測試每一個子任務;
2. 被分解的子任務能夠被不一樣的處理進程複用,避免代碼冗餘。(這裏說的不一樣的處理進程是指,針對不一樣的http請求,採用不一樣的子任務組合來處理)
3. 在複雜進程中添加、移除和替換子任務很是輕鬆,對已存在的進程沒有任何影響。
1. 雖然每一個子任務變得簡單了,可是當你再度嘗試將這些子任務組合成完整進程時有必定複雜性;
2. 你還須要保證獨立子任務測試經過後總體的流程能正常工做,這有必定的不肯定性。(由於在管道中流動的是http請求,而且子任務能夠修改http請求,這樣就存在前一次的修改內容致使下一個子任務的執行失敗的可能性)
3. 當你看到的都是一個個子任務時,對理解總體流程帶來困難(盲人摸象的故事想必你們很熟悉,正是此理)。
分析的文件:下面的部分是對Pipeline接口的定義,定義了send、through、via、then四個方法。(這裏不知道via方法具體是幹嗎的)
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
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 }
接下來,是對管道的實現.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 }
下面是對管道添加了異常處理的實現。
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 }
參考文獻:
1. Laravel 中管道設計模式的使用 —— 中間件實現原理探究
2. 不依賴於任何框架的管道