分析 Pipeline(1)

分析 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

再來就來看 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_reducearray_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