19
13

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 5 years have passed since last update.

Pipelineを使ったミドルウェアとコントローラーの実行箇所 前編 - Laravelコードリーディング

Last updated at Posted at 2018-05-31

本項のテーマ & 前置き

Laravel5.6のコードを読んでいきます。
LaravelのHttpKernelでミドルウェアとコントローラが実行されている箇所は、以下の通りです。

vendor/laravel/framework/src/Illuminate/Foundation/Http/Kernel.php

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メソッド

vendor/laravel/framework/src/Illuminate/Pipeline/Pipeline.php

public function send($passable)
{   
    $this->passable = $passable;

    return $this;
}

パイプラインを通じて送信するオブジェクトを渡しているだけです。パイプラインを通じて送信するオブジェクトというのは、このコンテキストでは当然、Illuminate\Http\Requestのオブジェクトです。

thoughメソッド

続いてthoughです。

vendor/laravel/framework/src/Illuminate/Pipeline/Pipeline.php

public function through($pipes)
{
    $this->pipes = is_array($pipes) ? $pipes : func_get_args();

    return $this;
}

$pipes引数でミドルウェアが渡されます。
注意が必要なのは、ここで渡されているミドルウェアはアプリケーション全体で利用されるミドルウェアです。これが定義されているのはApp\Http\Kernelクラスの以下の部分です。

app/Http/Kernel.php
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メソッド

vendor/laravel/framework/src/Illuminate/Pipeline/Pipeline.php

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プロパティ)に対して、このコールバック関数の処理が行われているわけです。確認してみましょう。

vendor/laravel/framework/src/Illuminate/Pipeline/Pipeline.php
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ミドルウェアを例にみてみましょう。

app/Http/Middleware/RedirectIfAuthenticated.php
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)です。

vendor/laravel/framework/src/Illuminate/Pipeline/Pipeline.php
protected function prepareDestination(Closure $destination)
{   
    return function ($passable) use ($destination) {
        return $destination($passable);
    };
}

$passable(この文脈では$requestが入ります)を引数に$destinationを実行するクロージャーを返しています。この$destinationはthenメソッドの引数$destinationに由来します。

Kernelクラスにもどってthenメソッドの引数を詳しくみてみましょう。

vendor/laravel/framework/src/Illuminate/Foundation/Http/Kernel.php

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を使ったミドルウェアとコントローラーの実行箇所 後編

終わりに

間違えあったら指摘おなしゃーす

19
13
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
19
13

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?