laravel

Laravel の Middleware とは?

Laravel laravel
LaravelのMiddlewareはどのように動いているのか?

さて、ここからが本題なのですが、Middleware の処理をソースコードから追って行く前に、まず簡単に「Middleware ってなんだっけ?」から復習がてらお話しようと思います。

Laravel の Middleware は、リクエストがアプリケーションに届いた時に、リクエストの前後で行われる一連の処理を指定する機能です。

Laravel で提供されている Middleware は以下の 3 種類で、

  1. システム全体で使用する Middleware=グローバル Middleware
  2. 特定のルートに対して適用する Middleware=ルート Middleware
  3. コントローラクラスのコンストラクタで指定する Middleware=コンストラクタ内 Middleware

です。

では、「その実態がどうなっているか?」というと app/Http/Kernel.php に定義されています。

そして、先ほど挙げた middleware(‘auth’)の際に実行されるのは、\App\Http\Middleware\Authenticate::class となります。

app/Http/Kernel.php

/**
 * The application's middleware aliases.
 *
 * Aliases may be used to conveniently assign middleware to routes and groups.
 *
 * @var array<string, class-string|string>
 */
protected $middlewareAliases = [
    'auth' => \App\Http\Middleware\Authenticate::class,
    'auth.basic' => \Illuminate\Auth\Middleware\AuthenticateWithBasicAuth::class,
    'auth.session' => \Illuminate\Session\Middleware\AuthenticateSession::class,
    'cache.headers' => \Illuminate\Http\Middleware\SetCacheHeaders::class,
    'can' => \Illuminate\Auth\Middleware\Authorize::class,
    'guest' => \App\Http\Middleware\RedirectIfAuthenticated::class,
    'password.confirm' => \Illuminate\Auth\Middleware\RequirePassword::class,
    'signed' => \App\Http\Middleware\ValidateSignature::class,
    'throttle' => \Illuminate\Routing\Middleware\ThrottleRequests::class,
    'verified' => \Illuminate\Auth\Middleware\EnsureEmailIsVerified::class,
];

Middleware が呼ばれるまでの道筋

  1. index.php
  2. vendor/laravel/framework/src/Illuminate/Foundation/Http/Kernel.php
  3. vendor/laravel/framework/src/Illuminate/Pipeline/Pipeline.php
  4. 各 Middleware

という流れになります。

先ほど確認した app/Http/Kernel.php の Middleware が呼ばれるタイミングはどこかというと、3 の Pipline.php が各 Middleware を呼び出している実体です。

では 1~4 を順に見て行きます。

index.php

Laravel のメイン処理は framework/public/index.php のごくわずかなコードです。
(この中を深く掘り下げていくとカオスですが)

framework/public/index.php

<?php

use Illuminate\Contracts\Http\Kernel;
use Illuminate\Http\Request;

define('LARAVEL_START', microtime(true));

/*
|--------------------------------------------------------------------------
| Check If The Application Is Under Maintenance
|--------------------------------------------------------------------------
|
| If the application is in maintenance / demo mode via the "down" command
| we will load this file so that any pre-rendered content can be shown
| instead of starting the framework, which could cause an exception.
|
*/

if (file_exists($maintenance = __DIR__.'/../storage/framework/maintenance.php')) {
    require $maintenance;
}

/*
|--------------------------------------------------------------------------
| Register The Auto Loader
|--------------------------------------------------------------------------
|
| Composer provides a convenient, automatically generated class loader for
| this application. We just need to utilize it! We'll simply require it
| into the script here so we don't need to manually load our classes.
|
*/

require __DIR__.'/../vendor/autoload.php';

/*
|--------------------------------------------------------------------------
| Run The Application
|--------------------------------------------------------------------------
|
| Once we have the application, we can handle the incoming request using
| the application's HTTP kernel. Then, we will send the response back
| to this client's browser, allowing them to enjoy our application.
|
*/

$app = require_once __DIR__.'/../bootstrap/app.php';

$kernel = $app->make(Kernel::class);

$response = $kernel->handle(
    $request = Request::capture()
)->send();

$kernel->terminate($request, $response);

で、中心のさらに中心である、Kernel について見て行きます。

Kernel.php

$kernel という変数名からも読み取れるように、これが Laravel の心臓です。

さて、このコードの表面だけ見ればものすごく違和感があります。

use Illuminate\Contracts\Http\Kernel;

//中略

$app = require_once __DIR__.'/../bootstrap/app.php';

$kernel = $app->make(Kernel::class);

$response = $kernel->handle(
    $request = Request::capture()
)->send();

$kernel->terminate($request, $response);

なぜなら、$kernel を make する際に使っているのは Illuminate\Contracts\Http\Kernel ですが、実際に中を見てみるとこいつは interface で実体を持っていません。

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

<?php

namespace Illuminate\Contracts\Http;

interface Kernel
{
    //中略
}

では「実体はどれか?」という話になるのですが、その実体は$kernel = $app->make(Kernel::class);の一つ前の処理、$app = require_once DIR.’/../bootstrap/app.php’;をみるとわかります。

その中に、以下のコードを見つけることができます。

bootstrap/app.php

$app->singleton(
    Illuminate\Contracts\Http\Kernel::class,
    App\Http\Kernel::class
);

つまり、bootstrap/app.php で Illuminate\Contracts\Http\Kernel::class の実体として App\Http\Kernel::class を登録しているため、$kernel = $app->make(Kernel::class);で$kernel が持つインスタンスは App\Http\Kernel になるというわけです。

では、App\Http\Kernel の中を見てみましょう。

App\Http\Kernel

<?php

namespace App\Http;

use Illuminate\Foundation\Http\Kernel as HttpKernel;

class Kernel extends HttpKernel
{
    /**
     * The application's global HTTP middleware stack.
     *
     * These middleware are run during every request to your application.
     *
     * @var array<int, class-string|string>
     */
    protected $middleware = [
        // \App\Http\Middleware\TrustHosts::class,
        \App\Http\Middleware\TrustProxies::class,
        \Illuminate\Http\Middleware\HandleCors::class,
        \App\Http\Middleware\PreventRequestsDuringMaintenance::class,
        \Illuminate\Foundation\Http\Middleware\ValidatePostSize::class,
        \App\Http\Middleware\TrimStrings::class,
        \Illuminate\Foundation\Http\Middleware\ConvertEmptyStringsToNull::class,
    ];

    /**
     * The application's route middleware groups.
     *
     * @var array<string, array<int, class-string|string>>
     */
    protected $middlewareGroups = [
        'web' => [
            \App\Http\Middleware\EncryptCookies::class,
            \Illuminate\Cookie\Middleware\AddQueuedCookiesToResponse::class,
            \Illuminate\Session\Middleware\StartSession::class,
            \Illuminate\View\Middleware\ShareErrorsFromSession::class,
            \App\Http\Middleware\VerifyCsrfToken::class,
            \Illuminate\Routing\Middleware\SubstituteBindings::class,
        ],

        'api' => [
            // \Laravel\Sanctum\Http\Middleware\EnsureFrontendRequestsAreStateful::class,
            \Illuminate\Routing\Middleware\ThrottleRequests::class.':api',
            \Illuminate\Routing\Middleware\SubstituteBindings::class,
        ],
    ];

    /**
     * The application's middleware aliases.
     *
     * Aliases may be used to conveniently assign middleware to routes and groups.
     *
     * @var array<string, class-string|string>
     */
    protected $middlewareAliases = [
        'auth' => \App\Http\Middleware\Authenticate::class,
        'auth.basic' => \Illuminate\Auth\Middleware\AuthenticateWithBasicAuth::class,
        'auth.session' => \Illuminate\Session\Middleware\AuthenticateSession::class,
        'cache.headers' => \Illuminate\Http\Middleware\SetCacheHeaders::class,
        'can' => \Illuminate\Auth\Middleware\Authorize::class,
        'guest' => \App\Http\Middleware\RedirectIfAuthenticated::class,
        'password.confirm' => \Illuminate\Auth\Middleware\RequirePassword::class,
        'signed' => \App\Http\Middleware\ValidateSignature::class,
        'throttle' => \Illuminate\Routing\Middleware\ThrottleRequests::class,
        'verified' => \Illuminate\Auth\Middleware\EnsureEmailIsVerified::class,
    ];
}

おや…?

Laravel の学習書などでよくみるクラスに行き着きましたね?

そうです。ここが Middleware を登録する場所です。

しかし、index.php によると$kernel は handle というメソッドを持つはずですがこのクラスには見当たりません。パニックパニック。

落ち着いてクラス宣言をみてください。

App\Http\Kernel

<?php

namespace App\Http;

use Illuminate\Foundation\Http\Kernel as HttpKernel;

class Kernel extends HttpKernel{
    //中略
}

どうやらスーパークラスが存在しているようです。

その実体は Illuminate\Foundation\Http\Kernel

このクラスの中を探すと…

Illuminate\Foundation\Http\Kernel

/**
 * Handle an incoming HTTP request.
 *
 * @param  \Illuminate\Http\Request  $request
 * @return \Illuminate\Http\Response
 */
public function handle($request)
{
    $this->requestStartedAt = Carbon::now();

    try {
        $request->enableHttpMethodParameterOverride();

        $response = $this->sendRequestThroughRouter($request);
    } catch (Throwable $e) {
        $this->reportException($e);

        $response = $this->renderException($request, $e);
    }

    $this->app['events']->dispatch(
        new RequestHandled($request, $response)
    );

    return $response;
}

はい、見つかりましたね。

ここから受け取ったリクエストを捌く全ての処理が始まります。

そしてこのコードを追っていくとどこかしらで Middleware の呼び出しが始まる箇所があるはずです。

本当はもっと丁寧に説明したいのですが、正直 Kernel のコードは膨大なので、Middleware に行き着く道筋だけ説明することにします。

結論としては、$response = $this->sendRequestThroughRouter($request);で Middleware の呼び出しが始まります。

App\Http\Kernel

/**
 * Send the given request through the middleware / router.
 *
 * @param  \Illuminate\Http\Request  $request
 * @return \Illuminate\Http\Response
 */
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());
}

new Pipeline($this->app)から始まるメソッドチェーンがそれです。

Pipeline.php

さて、、、つよつよ Laraveler 達から不評の Pipeline.php です。

/**
 * Set the object being sent through the pipeline.
 *
 * @param  mixed  $passable
 * @return $this
 */
public function send($passable)
{
    $this->passable = $passable;

    return $this;
}

/**
 * Set the array of pipes.
 *
 * @param  array|mixed  $pipes
 * @return $this
 */
public function through($pipes)
{
    $this->pipes = is_array($pipes) ? $pipes : func_get_args();

    return $this;
}

/**
 * Run the pipeline with a final destination callback.
 *
 * @param  \Closure  $destination
 * @return mixed
 */
public function then(Closure $destination)
{
    $pipeline = array_reduce(
        array_reverse($this->pipes()), $this->carry(), $this->prepareDestination($destination)
    );

    return $pipeline($this->passable);
}

/**
 * Get a Closure that represents a slice of the application onion.
 *
 * @return \Closure
 */
protected function carry()
{
    return function ($stack, $pipe) {
        return function ($passable) use ($stack, $pipe) {
            try {
                if (is_callable($pipe)) {
                    // If the pipe is a callable, then we will call it directly, but otherwise we
                    // will resolve the pipes out of the dependency container and call it with
                    // the appropriate method and arguments, returning the results back out.
                    return $pipe($passable, $stack);
                } elseif (! is_object($pipe)) {
                    [$name, $parameters] = $this->parsePipeString($pipe);

                    // If the pipe is a string we will parse the string and resolve the class out
                    // of the dependency injection container. We can then build a callable and
                    // execute the pipe function giving in the parameters that are required.
                    $pipe = $this->getContainer()->make($name);

                    $parameters = array_merge([$passable, $stack], $parameters);
                } else {
                    // If the pipe is already an object we'll just make a callable and pass it to
                    // the pipe as-is. There is no need to do any extra parsing and formatting
                    // since the object we're given was already a fully instantiated object.
                    $parameters = [$passable, $stack];
                }

                $carry = method_exists($pipe, $this->method)
                                ? $pipe->{$this->method}(...$parameters)
                                : $pipe(...$parameters);

                return $this->handleCarry($carry);
            } catch (Throwable $e) {
                return $this->handleException($passable, $e);
            }
        };
    };
}

順番に見ていきましょう。

まず、send ですが、これは passable に request をセットしています。

次に through ですが、これは pipes に middleware をセットしています。

そして、最後の then で middleware の処理が始まっていくのですが、これがめちゃくちゃわかりにくい処理になってます。

/**
 * Run the pipeline with a final destination callback.
 *
 * @param  \Closure  $destination
 * @return mixed
 */
public function then(Closure $destination)
{
    $pipeline = array_reduce(
        array_reverse($this->pipes()), $this->carry(), $this->prepareDestination($destination)
    );

    return $pipeline($this->passable);
}

この処理を順に理解するために、まずは PHP の array_reduce の動きについてつかんでおきたいと思います。

例えば以下のようなコードがあったとします。

$array = [1, 2, 3, 4, 5];
$sum = array_reduce($array, function($carry, $item) {
    echo $carry . <br>
    echo $item . <br>
    return $carry + $item;
}, 0);

echo $sum;

この時の出力結果は以下のようなイメージになります。

0 -> array_reduceの第3引数
1 -> array[0]

1 -> 前回の処理結果
2 -> array[1]

3 -> 前回の処理結果
3 -> array[2]

4 -> 前回の処理結果
6 -> array[3]

5 -> 前回の処理結果
10 -> array[4]

15 -> 最終結果

つまり、carry は前のクロージャーで実行された結果を返し、$item は array の配列を順にもらっているものです。

このことを頭に入れつつ、元のコードに戻りたいと思います。

Pipeline の then

ここで少し整理をしておきます。

array_reduce の第 1 引数に来るのは middleware で、Kernel.php に定義したものが入ってきます。

全ては書かないのですが、例えば以下の順に middleware が入ってきます。

  1. \App\Http\Middleware\TrustProxies::class
  2. \Illuminate\Http\Middleware\HandleCors::class
  3. \App\Http\Middleware\PreventRequestsDuringMaintenance::class

で、一旦 carry というクロージャーは置いておいて、第 3 引数に来るのはこれまた別の実行したい処理です。
(本質ではないので、ここでは仮に destination とラベリングします)

/**
 * Run the pipeline with a final destination callback.
 *
 * @param  \Closure  $destination
 * @return mixed
 */
public function then(Closure $destination)
{
    $pipeline = array_reduce(
        array_reverse($this->pipes()), $this->carry(), $this->prepareDestination($destination)
    );

    return $pipeline($this->passable);
}

なので、先ほどの例のイメージでいくと、carry に渡っていくものは、

  1. destination
  2. \App\Http\Middleware\TrustProxies::class
  3. \Illuminate\Http\Middleware\HandleCors::class
  4. \App\Http\Middleware\PreventRequestsDuringMaintenance::class

なのですが、今回は array_reverse がかかっているので、順番としては以下の通りになります。

  1. destination
  2. \App\Http\Middleware\PreventRequestsDuringMaintenance::class
  3. \Illuminate\Http\Middleware\HandleCors::class
  4. \App\Http\Middleware\TrustProxies::class

ここで、「あれ?middleware って配列に書いた順に処理されていくんじゃ…?しかも destination が最初に処理されたら middleware の意味がないような?」と思うでしょう。

そう、ここが Laravel の middleware が読み手を混乱させる要因なのです。

この疑問を解消するために、carry()の中を見て行きます。


/**
 * The method to call on each pipe.
 *
 * @var string
 */
protected $method = 'handle';

/**
 * Get a Closure that represents a slice of the application onion.
 *
 * @return \Closure
 */
protected function carry()
{
    return function ($stack, $pipe) {
        return function ($passable) use ($stack, $pipe) {
            try {
                if (is_callable($pipe)) {
                    // If the pipe is a callable, then we will call it directly, but otherwise we
                    // will resolve the pipes out of the dependency container and call it with
                    // the appropriate method and arguments, returning the results back out.
                    return $pipe($passable, $stack);
                } elseif (! is_object($pipe)) {
                    [$name, $parameters] = $this->parsePipeString($pipe);

                    // If the pipe is a string we will parse the string and resolve the class out
                    // of the dependency injection container. We can then build a callable and
                    // execute the pipe function giving in the parameters that are required.
                    $pipe = $this->getContainer()->make($name);

                    $parameters = array_merge([$passable, $stack], $parameters);
                } else {
                    // If the pipe is already an object we'll just make a callable and pass it to
                    // the pipe as-is. There is no need to do any extra parsing and formatting
                    // since the object we're given was already a fully instantiated object.
                    $parameters = [$passable, $stack];
                }

                $carry = method_exists($pipe, $this->method)
                                ? $pipe->{$this->method}(...$parameters)
                                : $pipe(...$parameters);

                return $this->handleCarry($carry);
            } catch (Throwable $e) {
                return $this->handleException($passable, $e);
            }
        };
    };
}

ここでみなさんの脳内メモリを補助します。
先ほどの array_reverse の結果を踏まえると、

  • passable = request
  • stack = destination
  • pipe = \App\Http\Middleware\PreventRequestsDuringMaintenance::class

ということになるので、ここまでみた感じだと「やっぱり destination が最初に動くじゃないか!」となってしまいます。

では、なぜそうならないかを順を追って説明します。

$carry = method_exists($pipe, $this->method)
                ? $pipe->{$this->method}(...$parameters)
                : $pipe(...$parameters);

このように pipe(クラス)に this->method のメソッドが存在するかをチェックし、存在していればそのメソッドを呼んでいます。

この method という変数は this なので、”handle”になっています。

みなさんご存知のように、「Laravel の middleware を作ったときに呼ばれるメソッドは handle」なのですが、その理由はここからきています。

では、試しに Route::middleware(‘auth’)でお馴染みの vendor/laravel/framework/src/Illuminate/Auth/Middleware/Authenticate.php の Middleware についてみてみます。

vendor/laravel/framework/src/Illuminate/Auth/Middleware/Authenticate.php

/**
 * Handle an incoming request.
 *
 * @param  \Illuminate\Http\Request  $request
 * @param  \Closure  $next
 * @param  string[]  ...$guards
 * @return mixed
 *
 * @throws \Illuminate\Auth\AuthenticationException
 */
public function handle($request, Closure $next, ...$guards)
{
    $this->authenticate($request, $guards);

    return $next($request);
}

わかりましたか?

各 middleware は引数で request と next を受け取り、次の middleware 呼び出しを行います。

つまり再起的に middleware が呼び出されているわけです。

そして、渡ってきた middleware の根に到達して初めて結果を戻し、stacktrace から抜けて行きます。

つまり、

  1. destination
  2. \App\Http\Middleware\PreventRequestsDuringMaintenance::class
  3. \Illuminate\Http\Middleware\HandleCors::class
  4. \App\Http\Middleware\TrustProxies::class

の順で呼び出しは起こるのですが、処理結果そのものは

  1. \App\Http\Middleware\TrustProxies::class
  2. \Illuminate\Http\Middleware\HandleCors::class
  3. \App\Http\Middleware\PreventRequestsDuringMaintenance::class
  4. destination

の順に返って行くのです。

以上が、Laravel が Middleware を呼び出している流れでした。

おわりに

デバッガーを使って処理を追いかけていると、一度のリクエストの間に Pipline の then は複数回走ります。

これは冒頭で Middleware には種類があるという話をしたかと思うのですが、グローバル Middleware->ルート Middleware..というようにそれぞれのタイミングで Middleware 達が実行されているからですね。

では、その順番がどう制御されているのか?ということについては詳しく調べていないのですが、おそらく then(Closure $destination)で渡ってくる$destination によって制御されているのでは?と考えています。

(つよつよ Laraveler、知っていたら教えてください)

タイトルとURLをコピーしました