在分析 bootstrap 流程的最後面的 handle()
時,有提到這段程式碼:
// 解析 request 並執行 Controller
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 在做什麼了。它與 DevOps 提到的 Pipeline 類似,是一個關卡接著一個關卡的流程。
從如何使用,來了解 Pipeline,或許是一個比較好的方法:
// 解析 request 並執行 Controller
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) {
// Do something
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)) {
// 如果是 callable 就呼叫吧
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 {
// 都不是的話,會期望 $pipe 是物件
$parameters = [$passable, $stack];
}
// 預設的 `method` 是 handle ,如果有使用 via() 的話可以調整。如果物件沒實作這個方法的話,就會假設它有實作 __invoke。
$response = method_exists($pipe, $this->method)
? $pipe->{$this->method}(...$parameters)
: $pipe(...$parameters);
// 如果 response 是 Responsable 的話,就傳入 request 轉 response;不然就直接回傳了
return $response instanceof Responsable
? $response->toResponse($this->container->make(Request::class))
: $response;
};
};
}
裡面的流程都很單純,難的地方在最外層是 Closure 包 Closure。先假設 array ,並用比較簡單的寫法把它改成 inline 試試:
// 從上面的原始碼得知,這個其實是 middleware 的 handle 實作
$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'); // return 'response321'
由我們對 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 的變數會被包起來,接著再傳給下一個:
// 這次的 stack 就是上面的 return
$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
。