本項のテーマ & 前置き
Laravel5.6のコードを読んでいきます。
LaravelのHttpKernelでミドルウェアとコントローラが実行されている箇所は、以下の通りです。
protected function sendRequestThroughRouter($request)
{
$this->app->instance('request', $request);
Facade::clearResolvedInstance('request');
$this->bootstrap();
/// ココだよ、ココ
return (new Pipeline($this->app))
->send($request)
->through($this->app->shouldSkipMiddleware() ? [] : $this->middleware)
->then($this->dispatchToRouter());
}
public/index.phpからこのメソッドに来るまでの流れについては別の記事にまとめてありますので、ご参考ください。
Laravelコードリーディング - public/index.phpからミドルウェア/コントローラーが実行されるまで
さて、Pipelineというクラスでなにやらやってます。このPipelineをつかってどういう風にミドルウェアやコントローラーの処理が実行されているのかを調べていくのが本項のテーマです。
前編ではミドルウェアが実行される仕組みを実際のコードを見ながら確認します。コントローラーが実際に実行される箇所までは前編では到達しませんでした。。。記事の分量的に長くなってしまったので、これについては後編にまわします。
すでに前編を読んだ方、後編はこちらからどうぞ
Laravelコードリーディング - Pipelineを使ったミドルウェアとコントローラーの実行箇所 後編
Let's コードリーディング!!
さて、まずはPipelineクラスから読んでいきたいと思います。
Pipelineってなんぞ?
参考ページから読み解く
コマンドラインでパイプを使ったことがある人は多いかと思いますが、そのパイプからきてるみたいです。以下、とてもわかりやすい記事があったので紹介します。
How to use the Pipeline Design Pattern in Laravel
抜粋してみましょう。Google翻訳つき。
The Pipeline Design Pattern is where data is passed through a sequences of tasks or stages. The pipeline acts like an assembly-line, where the data is processed and then passed on to the next stage.
パイプラインデザインパターンは、データが一連のタスクまたはステージを通過する場所です。パイプラインは、データが処理され、次のステージに渡されるアセンブリラインのように機能します。
Using a pipeline is advantageous because it is really easy to compose a complex process as individual tasks. It also makes it easy to add, remove, or replace stages within the pipeline without disturbing the entire process.
パイプラインを使用することは、個々のタスクとして複雑なプロセスを構成することが実際に容易であるため、有利です。また、プロセス全体を妨げることなく、パイプライン内のステージを簡単に追加、削除、または置き換えることができます。
つまり、パイプラインって?
再利用可能なタスクを組み合わせることで複雑なプロセスを構成しつつも、タスクの組み合わせの変更がプロセス全体には影響がないようにする仕組みってことですね。
Laravelに置き換えると、
プロセス全体 :Httpリクエストを受けてHttpレスポンスを返す。
再利用可能なタスク :ミドルウェアの処理。
タスクの組み合わせの変更:ルーティングによって処理されるミドルウェアが変わる
ってことかと思います。
Pipelineクラス
実際のPipelineクラスを確認...する前にちょっとやっておきたいことがあります。HttpKernelでPipelineを使っている箇所をちょっと書き換えて読みやすくしてみましょう。
/// 書き換え前
return (new Pipeline($this->app))
->send($request)
->through($this->app->shouldSkipMiddleware() ? [] : $this->middleware)
->then($this->dispatchToRouter());
/// 書き換え後
$pipeline = new Pipeline($this->app);
$pipeline->send($request);
$pipeline->through($this->app->shouldSkipMiddleware() ? [] : $this->middleware);
$ret = $pipeline->then($this->dispatchToRouter());
return $ret;
メソッドチェーンを使っていた箇所を分けただけです。
ではPipelineを読み進めてみましょう。
HttpKernelで利用されているIlluminate\Routing\Pipeline\PipelineクラスはIlluminate\Pipeline\Pipelineクラスを継承しています。ここで使われているsend/through/thenの3つのメソッドはIlluminate\Pipeline\Pipelineの方に実装されています。
sendメソッド
public function send($passable)
{
$this->passable = $passable;
return $this;
}
パイプラインを通じて送信するオブジェクトを渡しているだけです。パイプラインを通じて送信するオブジェクトというのは、このコンテキストでは当然、Illuminate\Http\Requestのオブジェクトです。
thoughメソッド
続いてthoughです。
public function through($pipes)
{
$this->pipes = is_array($pipes) ? $pipes : func_get_args();
return $this;
}
$pipes引数でミドルウェアが渡されます。
注意が必要なのは、ここで渡されているミドルウェアはアプリケーション全体で利用されるミドルウェアです。これが定義されているのはApp\Http\Kernelクラスの以下の部分です。
protected $middleware = [
\Illuminate\Foundation\Http\Middleware\CheckForMaintenanceMode::class,
\Illuminate\Foundation\Http\Middleware\ValidatePostSize::class,
\App\Http\Middleware\TrimStrings::class,
\Illuminate\Foundation\Http\Middleware\ConvertEmptyStringsToNull::class,
\App\Http\Middleware\TrustProxies::class,
];
thenメソッド
public function then(Closure $destination)
{
$pipeline = array_reduce(
array_reverse($this->pipes), $this->carry(), $this->prepareDestination($destination)
);
return $pipeline($this->passable);
}
実際に処理が実行される箇所です。sendとthroughはこの前準備だったワケです。
array_reduceについての詳細はこちらをご覧ください。
carryメソッドではarray_reduceのコールバックを返しています。つまり、$this->pipes
(ここではミドルウェアの一覧。具体的にはApp\Http\Kernelの$middlewareプロパティ)に対して、このコールバック関数の処理が行われているわけです。確認してみましょう。
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;
};
};
}
ちょっとごちゃごちゃしているので注目すべき箇所を抜粋してみます。
$response = method_exists($pipe, $this->method)
? $pipe->{$this->method}(...$parameters)
: $pipe(...$parameters);
$this->method
のデフォルトはhandleです。Pipelineクラスのviaメソッドを利用することでこの値を変更することもできますが、今回の文脈では利用していません。
つまり、ここがミドルウェアのhandleメソッドが呼び出される箇所になります。
とはいえ、ここではまだ実行されてません。あくまで複数のミドルウェアのhandleメソッドを1つのクロージャーにまとめる作業をしているだけです。
まとめられたクロージャーを模式的に書くのであれば以下のようになるかと思います。
/// middleware1〜3があると仮定
$pipeline = function($reqeust) {
return middleware1->handle($request, function() {
return middleware2->handle($request, function() {
return middleware3->handle($request, function() {
return $destination($request);
}
})
})
}
まるでたまねぎみたいです。(実際にLaravelのコメントの中ではthe Closure onionという表現を使ったりしてる箇所もあったりします)
ミドルウェアを作ったことがある人ならご存知かと思いますが、ミドルウェアのhandleメソッドでは通常、第2引数のクロージャーを呼び出します。RedirectIfAuthenticatedミドルウェアを例にみてみましょう。
public function handle($request, Closure $next, $guard = null)
{
if (Auth::guard($guard)->check()) {
return redirect($guard.'/home');
}
return $next($request);
}
$next($request)
の箇所がそうです。これで次のミドルウェアのhandleメソッドを呼び出しているということになります。
では、middleware3(つまり最後のミドルウェア)のhadleメソッドに渡されるクロージャーはなんなんでしょうか?それはarray_reduceの第3引数、つまり$this->prepareDestination($destination)
です。
protected function prepareDestination(Closure $destination)
{
return function ($passable) use ($destination) {
return $destination($passable);
};
}
$passable
(この文脈では$requestが入ります)を引数に$destination
を実行するクロージャーを返しています。この$destination
はthenメソッドの引数$destination
に由来します。
Kernelクラスにもどってthenメソッドの引数を詳しくみてみましょう。
protected function sendRequestThroughRouter($request)
{
~~~ 略
return (new Pipeline($this->app))
->send($request)
->through($this->app->shouldSkipMiddleware() ? [] : $this->middleware)
->then($this->dispatchToRouter());
}
protected function dispatchToRouter()
{
return function ($request) {
$this->app->instance('request', $request);
return $this->router->dispatch($request);
};
}
$this->router
というのはこのKernelクラスにコンストラクタインジェクションされた依存オブジェクトで、Illuminate\Routing\Routerクラスです。
後編に続く
ここまででミドルウェアが実行される仕組みは確認できたかと思います。ここから先はRouterクラスを追いかけて、Controllerのアクションメソッドが実行されるまでを明らかにしていくわけですが、これについては後編に譲りたいと思います。
Laravelコードリーディング - Pipelineを使ったミドルウェアとコントローラーの実行箇所 後編
終わりに
間違えあったら指摘おなしゃーす