分析 Pipeline(2)

昨天使用範例說明 Pipeline 的包裝方法,相信至少可以略懂個一二。接下來先補充一下 parsePipeString() 在做什麼。

protected function parsePipeString($pipe)
{
// 使用冒號 `:` 把 $pipe 拆成兩個元素塞到 $name 與 $parameters,沒得拆的話會使用 [] 補到 $parameters 裡
list($name, $parameters) = array_pad(explode(':', $pipe, 2), 2, []);

// 如果有拆成的話,這會是字串,再把它用逗號拆成 array
if (is_string($parameters)) {
$parameters = explode(',', $parameters);
}

return [$name, $parameters];
}

昨天有提到,Pipeline 其實正是 middleware 的實作,而在 Laravel 樣版裡,Http Kernel 其實有定義一個 middleware 是這個:

throttle:60,1

是的,就是它!於是 throttle 會被轉成 $name 傳入 $app->make(),$parameters 則會補到 [$passable, $stack] 後面,成為參數的一部分。同時這也是有的 middleware 如 ThrottleRequests 為什麼 handle() 可以接這麼多參數的原因。


接著我們來看 Hub

它與 Pipeline 的關係圖如下:

@startuml
interface Illuminate\Contracts\Pipeline\Pipeline {
+ {abstract} send($traveler)
+ {abstract} through($stops)
+ {abstract} via($method)
+ {abstract} then(Closure $destination)
}

interface Illuminate\Contracts\Pipeline\Hub {
+ {abstract} pipe($object, $pipeline = null)
}

Illuminate\Contracts\Pipeline\Pipeline <|.. Illuminate\Pipeline\Pipeline
Illuminate\Contracts\Pipeline\Hub <|.. Illuminate\Pipeline\Hub
Illuminate\Pipeline\Pipeline <|-- Illuminate\Routing\Pipeline
Illuminate\Pipeline\Pipeline <- Illuminate\Pipeline\Hub
@enduml

先看一下 pipe() 的實作:

public function pipe($object, $pipeline = null)
{
$pipeline = $pipeline ?: 'default';

return call_user_func(
$this->pipelines[$pipeline], new Pipeline($this->container), $object
);
}

從這裡可以了解,$this->pipelines[$pipeline] 實際要放的 Closure 應該要長的像這樣:

function(Pipeline $pipeline, $object) {

}

它的用途大概是,我們可以定義很多種流程,然後依不同的情境執行不同的流程。

$hub = $app->make(Hub::class);

if ($request->isAjax()) {
return $hub->pipe($request, 'ajax');
}

return $hub->pipe($request);

而事實上,Laravel 預設樣版並沒有使用 Hub 的功能。


最後來看一下 Routing 的繼承實作,來看其中一段加上例外處理的程式碼:

try {
return $destination($passable);
} catch (Exception $e) {
return $this->handleException($passable, $e);
} catch (Throwable $e) {
return $this->handleException($passable, new FatalThrowableError($e));
}

這裡可以看到會由 handleException() 來處理例外,來看一下這段程式碼,會有意外的發現:

protected function handleException($passable, Exception $e)
{
if (! $this->container->bound(ExceptionHandler::class) ||
! $passable instanceof Request) {
throw $e;
}

$handler = $this->container->make(ExceptionHandler::class);

$handler->report($e);

$response = $handler->render($passable, $e);

if (method_exists($response, 'withException')) {
$response->withException($e);
}

return $response;
}

如果還有印象的話,ExceptionHandler 正是分析 bootstrap 流程一開始的「綁定實作」之一,也就是 $app->singleton() 所綁定的其中一個類別名。這裡可以發現,它會呼叫 report() 以及 render(),剛好就是 Laravel 樣版的 Handler 實作的一部分。

namespace App\Exceptions;

use Exception;
use Illuminate\Foundation\Exceptions\Handler as ExceptionHandler;

class Handler extends ExceptionHandler
{
public function report(Exception $exception)
{
parent::report($exception);
}

public function render($request, Exception $exception)
{
return parent::render($request, $exception);
}
}

我們知道文件裡有寫如何使用 Exception Handler,而實際上抓 Exception 並轉交給 Handler 處理的實作就是在這裡。

今日總結

這兩天 Pipeline 的 Closure 實作,並不是那麼好懂,不過當理解之後,Golang 或 Javascript 之類的語言,也都能使用類似的方法實作 middleware 哦。