在分析 bootstrap 流程的最後面的 handle()
時,有提到一段程式碼。
return (new Pipeline($this->app)) ->send($request) ->through($this->app->shouldSkipMiddleware() ? [] : $this->middleware) ->then($this->dispatchToRouter());
|
是的,今天要來分析上面看到的 Pipeline
。
類別圖
Laravel 5.7 裡,跟 Pipeline 相關的主要角色有三個:
雖然還有 Hub,不過它很簡單,所以先不提。
類別圖如下:
PlantUML 原始碼:
@startuml interface Illuminate\Contracts\Pipeline { + {abstract} send($traveler) + {abstract} through($stops) + {abstract} via($method) + {abstract} then(Closure $destination) }
Illuminate\Contracts\Pipeline <|.. Illuminate\Pipeline\Pipeline Illuminate\Pipeline\Pipeline <|-- Illuminate\Routing\Pipeline @enduml
|
繼承與實作關係很單純,跟 Application 一樣,可以了解一下繼承有沒有符合里氏替換原則。
參考註解,Routing\Pipeline
繼承並沒有修改原有行為,而是為了加上 try/catch,等後面一點再來分析這個類別。
Pipeline
再來就來看 Pipeline 在做什麼了。它與 DevOps 提到的 Pipeline 類似,是一個關卡接著一個關卡的流程。
從如何使用,來了解 Pipeline,或許是一個比較好的方法:
return (new Pipeline($this->app)) ->send($request) ->through($this->app->shouldSkipMiddleware() ? [] : $this->middleware) ->then($this->dispatchToRouter());
|
首先是 send()
,它非常簡單,只是先保存好一個原物料來當輸入。HTTP Kernel 使用 request 作為輸入。
public function send($passable) { $this->passable = $passable;
return $this; }
|
再來 through()
,則是定義有什麼樣的「水管」,HTTP Kernel 使用 middleware 作為水管。
public function through($pipes) { $this->pipes = is_array($pipes) ? $pipes : func_get_args();
return $this; }
|
最後 then()
,會傳入一個作為水管最後「目標」的 Closure。HTTP Kernel 使用 $this->dispatchToRouter()
的結果作為目標。
public function then(Closure $destination) { $pipeline = array_reduce( array_reverse($this->pipes), $this->carry(), $this->prepareDestination($destination) );
return $pipeline($this->passable); }
|
而這裡的實作正是最近幾天最難理解的。array_reduce()
的功能是把一個 array 拆分成個別元素,然後依序傳入某個 callable,每個元素的輸出,都會成為下個元素的輸入,最終轉化成另一種結果。而第一個元素的輸入是可以自己指定的。而 callback 的格式如下:
function($carry, $value) { return $newCarry; }
|
可以開始看程式了,首先看比較好懂的 prepareDestination()
。
protected function prepareDestination(Closure $destination) { return function ($passable) use ($destination) { return $destination($passable); }; }
|
它其實就只是再包裝過一次,會這麼做的理由是為了讓 Routing\Pipeline
包一層 try/catch。
再來就是最困難的 carry()
。
protected function carry() { return function ($stack, $pipe) { return function ($passable) use ($stack, $pipe) { if (is_callable($pipe)) { return $pipe($passable, $stack); } elseif (! is_object($pipe)) { list($name, $parameters) = $this->parsePipeString($pipe);
$pipe = $this->getContainer()->make($name);
$parameters = array_merge([$passable, $stack], $parameters); } else { $parameters = [$passable, $stack]; }
$response = method_exists($pipe, $this->method) ? $pipe->{$this->method}(...$parameters) : $pipe(...$parameters);
return $response instanceof Responsable ? $response->toResponse($this->container->make(Request::class)) : $response; }; }; }
|
裡面的流程都很單純,難的地方在最外層是 Closure 包 Closure。先假設 array,並用比較簡單的寫法把它改成 inline 試試:
$pipe = [ function($request, $next) { return $next($request) . '1'; }, function($request, $next) { return $next($request) . '2'; }, function($request, $next) { return $next($request) . '3'; }, ];
$pipeline = array_reduce( array_reverse($pipe), function ($stack, $pipe) { return function ($passable) use ($stack, $pipe) { return $pipe($passable, $stack); }; }, function ($passable) { return 'response'; } );
$pipeline('request');
|
由我們對 array_reduce
與 array_reverse
的理解,可以知道第二個 callback 被執行了三次,我們試著把執行過程展開來看看。
第一次執行的情況是這樣的:
$stack0 = function ($passable) { return 'response'; };
$pipe3 = function($request, $next) { return $next($request) . '3'; }
return function ($passable) use ($stack0, $pipe3) { return $pipe3($passable, $stack0); };
|
單看這段程式碼,可以知道 $pipe3 的 $next,實際上就是 $stack0。所以回傳的 Closure 執行結果會是 response3
。
根據 Closure 的特性,我們可以知道回傳的 Closure 的變數會被包起來,接著再傳給下一個:
$stack3 = function ($passable) { return $pipe3($passable, $stack0); };
$pipe2 = function($request, $next) { return $next($request) . '2'; }
return function ($passable) use ($stack3, $pipe2) { return $pipe2($passable, $stack3); };
|
跟上面類似,$pipe2 的 $next 其實就是 $stack3,執行結果會是 response32
。依此類推最後一次:
$stack2 = function ($passable) { return $pipe($passable, $stack); };
$pipe1 = function($request, $next) { return $next($request) . '1'; }
return function ($passable) use ($stack2, $pipe1) { return $pipe1($passable, $stack2); };
|
$pipe1 的 $next 其實就是 $stack2,執行結果就是 response321
。
這個過程就很像是在遞迴(recursion),但又比遞迴更為神奇的寫法。目前無法確定為何要使用這麼難理解的寫法,也許目的是為了效能。
其他套件也有類似的做法,如 GuzzleHttp\Middleware
也是用類似的寫法。而 Slim Framework 也有實作 Middleware,但它在 3.8.x 之前是使用 SplStack 存放 Closure,後來 3.9 開始才改用類似的寫法。
今天先看到這裡,明天繼續看 Routing\Pipeline
。