🐘

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

2023/04/26に公開

はじめに

こちらの記事の続編で Laravel Breeze の Remember me 編を書こうとしていました。

https://zenn.dev/yskn_sid25/articles/5ac107b0f1cd7d

その際にソースを追っかけていたのですが、Remember me について解説するためには、auth ミドルウェアを見る必要がありました。

そして、Middleware 見るためには Laravel の呼び出しライフサイクルから見ていく必要があったのですが、結果として Remember me よりも複雑でしたので別記事として公開することにしました。

なので、Remember me 編に入るまでにこちらの記事を読んでおくと、スムーズかと思います。

また内容が Laravel のコアな部分についてなので、つよつよ Laraveler 達に

「クソ記事」「こんな程度で技術記事書いてるとか」

みたいに叩かれそうで怯えながら公開したのですが、Twitter でびっくりするくらい沢山の良い反応を頂けています!

反応してくださった皆様、そして記事を読んでくださった皆様本当にありがとうございます!

https://twitter.com/samurai_se/status/1651164918069223426

以下、つよつよ Laraveler 達からのお褒めの言葉です。

https://twitter.com/mpyw/status/1651174956913541129

https://twitter.com/KentarouTakeda/status/1651173647934169088

https://twitter.com/hanhan1978/status/1651165251281485824

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、知っていたら教えてください)

Pipeline は沼

さらに余談ですが、この記事を書くために Middleware を調べていた途中こんなツイートをしました。

https://twitter.com/samurai_se/status/1650833290876841984

自分自身調べていて思いましたが、再帰を使った処理って一見かっこいいようですがその実デバッグがクソしづらいのでただの自己満足 自分の CPU(頭脳)が追いつかずなかなか理解するのに時間がかかりました。

他の人はどうなんだろうと思いきや、自分の知る つよつよ Laraveler たちが先のツイートに反応してくださりまして、いずれも「Pipeline は沼」という見解で一致しているようです。

いくつかツイートをご紹介します。

@mpyw

https://twitter.com/mpyw/status/1650834567849455616

@KentarouTakeda

https://twitter.com/KentarouTakeda/status/1650853978610216965

@naopusyu

https://twitter.com/naopusyu/status/1650848381923889153

本編

この記事の本来の目的は、↓ の記事をより容易に理解するためのものでした。

https://zenn.dev/yskn_sid25/articles/03fb71dfcf82d6

当初は ↑ の記事内で解説しようと思っていたのですが、そうすると 45,000 字ほどの膨大なレポートが出来上がってしまうため、記事を二つに分割しました。

その結果、本来おまけだったこの記事が完全に本編になってしまいました 😂

なのでよければ ↑ の記事も読んでやってください。

メンバー募集中!

サーバーサイド Kotlin コミュニティを作りました!

Kotlin ユーザーはぜひご参加ください!!

https://serverside-kt.connpass.com/

また関西在住のソフトウェア開発者を中心に、関西エンジニアコミュニティを一緒に盛り上げてくださる方を募集しています。

よろしければ Conpass からメンバー登録よろしくお願いいたします。

https://blessingsoftware.connpass.com/

blessing software

Discussion