Laravelのミドルウェアの実施順検証と小技

  • 21
    いいね
  • 0
    コメント

こんにちはみなさん

以前に@remore に「ミドルウェアってなんなんすかね?」って聞いたら、「ミドルウェアほど幅広く使われる言葉はそうないよね」と応えてきました。

一番初めに私が聞いたミドルウェアの意味は、アプリケーションが動くための周辺機構・・・DBとかキャッシュとかを表していましたが、最近ミドルウェアと聞くと、アプリケーションに於いてリクエストからアプリの実処理に行く間に割り込む中間処理とかフィルタリングとかを意味しているようです。
随分前に、以下のような絵を書いて、自分でミドルウェアについて納得したものです。
koa_middleware.jpg
今回は、この絵を利用してミドルウェアの動作を再確認しつつ、Laravelのミドルウェアで使える小技を幾つか紹介してみましょう。
自分の持っているLaravelが5.2なので、今回もLaravel5.2を使います。

TL;DR

  1. Laravelのミドルウェアは処理の前後に割り込んで行われる処理を実装するもの
  2. Laravelのミドルウェアの実施順は前処理と後処理で逆順になる
  3. ミドルウェアを使うと時間計測やレスポンスフィールドの追加とかが簡単にできる

Laravelのミドルウェア

ミドルウェアの基本

Laravelのミドルウェアは一種のアスペクト指向を実現するための機構で、アクションログを出したり、認証チェックなどの全体もしくは一部の処理で共通して必要な前後処理を割り込ませる装置です。
Laravelのミドルウェアはやっぱりartisanを使ってスケルトンを作ることができます。

$ php artisan make:middleware OneMiddleware

これで、app/Http/Middleware/OneMiddleware.phpにミドルウェアが生成されます。
ミドルウェアのクラスにはhandleメソッドがあり、ここでミドルウェアでするべき処理を記載できます。

OneMiddleware.php
class OneMiddleware
{
    /**
     * Handle an incoming request.
     *
     * @param  \Illuminate\Http\Request  $request
     * @param  \Closure  $next
     * @return mixed
     */
    public function handle($request, Closure $next)
    {
        // ここで前処理を行う
        $res = $next($request); // ここで次のミドルウェアもしくは実処理に移動
        // ここで必要であれば後処理
        return $res;
    }
}

ミドルウェアの登録と実施順

ミドルウェアの設定は主にapp/Http/Kernel.phpで行います。

全ての処理に共通して処理を行うミドルウェア

ミドルウェアが全ての処理に対して前後処理を行う場合は、Kernelの$middlewareに処理を実施するミドルウェアのクラスを登録します。

app/Http/Kernel.php
    protected $middleware = [
        \Illuminate\Foundation\Http\Middleware\CheckForMaintenanceMode::class,
        Middleware\OneMiddleware::class,
        Middleware\TwoMiddleware::class,
    ];

ここで登録されたミドルウェアはRouterで指定などしなくても、どんな処理に対しても割り込んで追加処理を行います。

複数のミドルウェアをまとめておく

幾つかのミドルウェアはいくつか組み合わせて使われる場合があります。この場合はKernelの$middlewareGroupsに名前をつけてまとめておくことができます。

app/Http/Kernel.php
    protected $middlewareGroups = [
        'num' => [
            Middleware\ThreeMiddleware::class,
            Middleware\ForeMiddleware::class,
        ]
    ];

単体で使うミドルウェアを登録しておく

各Routerで個別に使用されるミドルウェアは$routeMiddlewareに名前をつけて登録しておきます。

App\Http\Kernel.php
    protected $routeMiddleware = [
        'five' => Middleware\FiveMiddleware::class,
        'six'  => Middleware\SixMiddleware::class,
    ];

これで、あとでRouterで使用を宣言すれば使えるようになります。

ミドルウェアをRouterに設定する

登録したミドルウェアをRouterに設定しましょう。
これまで登録してきた各ミドルウェアのhandleメソッドに次のコードを設定し、各ミドルウェアがどのように動作するか見てみましょう。

Middleware.php
    public function handle($request, Closure $next)
    {
        \Log::debug(get_class());
        $res = $next($request); // ここで次のミドルウェアもしくは実処理に移動
        \Log::debug(get_class());
        return $res;
    }

Routerで普通に指定する

まず、次のようにRouterに直接ミドルウェアを設定してみましょう。

routes.php
Route::get('/some', ['middleware' => 'num', function () {
    return ['data' => true];
}]);

ミドルウェアの設定はこんな感じでKernelで設定したグループかミドルウェアの名前を指定してあげます。
これに対してcurlでどのようなログが出るかを見てみます。

$ curl http://127.0.0.1/some
# log
[2016-12-05 22:57:37] local.DEBUG: App\Http\Middleware\OneMiddleware
[2016-12-05 22:57:37] local.DEBUG: App\Http\Middleware\TwoMiddleware
[2016-12-05 22:57:37] local.DEBUG: App\Http\Middleware\ThreeMiddleware
[2016-12-05 22:57:37] local.DEBUG: App\Http\Middleware\ForeMiddleware
[2016-12-05 22:57:37] local.DEBUG: App\Http\Middleware\ForeMiddleware
[2016-12-05 22:57:37] local.DEBUG: App\Http\Middleware\ThreeMiddleware
[2016-12-05 22:57:37] local.DEBUG: App\Http\Middleware\TwoMiddleware
[2016-12-05 22:57:37] local.DEBUG: App\Http\Middleware\OneMiddleware

まず前処理については、全体で共有されるミドルウェアの処理が実行され、ついでRouterで指定されたミドルウェア(ここではグループで指定したもの)が実行されています。
続いて実処理が終了した後の後処理については、前処理で実行された順番とは逆順で実行されています。
この辺ははじめに見せた図にもあるような層構造のミドルウェアを思い浮かべると、しっくりきます。

ミドルウェア複数指定

ミドルウェアを複数指定するときは以下のように配列で指定できます。

routes.php
Route::get('/more', ['middleware' => ['num', 'five'], function () {
    return ['data' => true];
}]);

これに対してcurlを打つと以下のようになります

$ curl http://127.0.0.1/more
# log
[2016-12-05 23:12:13] local.DEBUG: App\Http\Middleware\OneMiddleware
[2016-12-05 23:12:13] local.DEBUG: App\Http\Middleware\TwoMiddleware
[2016-12-05 23:12:13] local.DEBUG: App\Http\Middleware\ThreeMiddleware
[2016-12-05 23:12:13] local.DEBUG: App\Http\Middleware\ForeMiddleware
[2016-12-05 23:12:13] local.DEBUG: App\Http\Middleware\FiveMiddleware
[2016-12-05 23:12:13] local.DEBUG: App\Http\Middleware\FiveMiddleware
[2016-12-05 23:12:13] local.DEBUG: App\Http\Middleware\ForeMiddleware
[2016-12-05 23:12:13] local.DEBUG: App\Http\Middleware\ThreeMiddleware
[2016-12-05 23:12:13] local.DEBUG: App\Http\Middleware\TwoMiddleware
[2016-12-05 23:12:13] local.DEBUG: App\Http\Middleware\OneMiddleware

Routerグループで指定

Routerのグループでミドルウェアを指定すると、必要なミドルウェアが同じである処理をまとめておくことができます。
また、Routerのメソッドチェーンでミドルウェアを指定することもできます。
まとめると以下のような書き方になります。

routes.php
Route::group(['middleware' => 'num'], function () {
    Route::get('/other', ['middleware' => 'five', function () {
        return ['data' => true];
    }])->middleware(['six']);
});

流石にこんな風に書くことはないと思いますが、curlを打って、動作を確認してみましょう。

$ curl http://127.0.0.1/other
# log
[2016-12-05 23:17:43] local.DEBUG: App\Http\Middleware\OneMiddleware
[2016-12-05 23:17:43] local.DEBUG: App\Http\Middleware\TwoMiddleware
[2016-12-05 23:17:43] local.DEBUG: App\Http\Middleware\ThreeMiddleware
[2016-12-05 23:17:43] local.DEBUG: App\Http\Middleware\ForeMiddleware
[2016-12-05 23:17:43] local.DEBUG: App\Http\Middleware\FiveMiddleware
[2016-12-05 23:17:43] local.DEBUG: App\Http\Middleware\SixMiddleware
[2016-12-05 23:17:43] local.DEBUG: App\Http\Middleware\SixMiddleware
[2016-12-05 23:17:43] local.DEBUG: App\Http\Middleware\FiveMiddleware
[2016-12-05 23:17:43] local.DEBUG: App\Http\Middleware\ForeMiddleware
[2016-12-05 23:17:43] local.DEBUG: App\Http\Middleware\ThreeMiddleware
[2016-12-05 23:17:43] local.DEBUG: App\Http\Middleware\TwoMiddleware
[2016-12-05 23:17:43] local.DEBUG: App\Http\Middleware\OneMiddleware

実施順のまとめ

Laravelに於けるミドルウェアの前処理の実施順は以下のとおりです。

  1. Kernelで設定した全体共通のミドルウェア
  2. Routerのグループで指定したミドルウェア
  3. Routerのルーティングメソッドの第2変数で指定したミドルウェア
  4. Routerのメソッドチェーンで指定したミドルウェア

また、後処理はこの逆順で走ります。
この辺の動作は以前にKoaで検証したものと同様でした。

小技

実行時間を計測する

ミドルウェアの前後処理を利用することで、実処理の実行時間を計測することができます。
以下のようなミドルウェアを作成しましょう。

$ php artisan make:middleware TimerMiddleware
TimerMiddleware.php
    public function handle($request, Closure $next)
    {
        $before = microtime(true);
        $res = $next($request);
        \Log::debug(microtime(true) - $before);
        return $res;
    }

前処理で現在の時間をmicrotimeで取得し、後処理で現在の時刻との差をログに出力しています。
これを以下のようにRouterに登録すると、実行時間を計測できます。

routes.php
Route::get('/timer', function() {
    sleep(1);
    return ['data' => true];
})->middleware(['timer']);

curlを打つとこんな感じになります。

$ curl http://127.0.0.1/timer
# log
[2016-12-05 23:31:58] local.DEBUG: 1.0282618999481

これで実処理のコードを汚さずに、処理の実行時間が計測できます。
例えば、開発時には全体共通で実行時間を計測し、本番に上げるときにミドルウェアから外してしまうという運用もありかもしれません。

APIの共通フィールドを追加

APIに共通フィールドを設定したい時があると思います。
そんなときにミドルウェアを使うと便利かもしれません。

php artisan make:middleware AddFieldMiddleware

ミドルウェアの処理は以下の通りです。

AddFieldMiddleware.php
    public function handle($request, Closure $next)
    {
        $before = microtime(true);
        $res = $next($request);
        $pass = microtime(true) - $before;
        $data = $res->original;
        $data['time'] = $pass;
        $data['is_maintenance'] = false;
        $res->setContent($data);
        return $res;
    }

Routerに入れてみます。複数のAPIで共通していれる可能性があるので、今回はグループ化してみます。

routes.php
Route::group(['middleware' => 'api_base'], function () {
    Route::get('/another', function () {
        sleep(1);
        return ['data' => true];
    });
});

それでは実動作のレスポンスを見てみましょう。

$ curl http://127.0.0.1/another
{"data":true,"time":1.0272550582886,"is_maintenance":false}

フィールドが追加されています。

まとめ

Laravelのミドルウェアを少し真面目に検証してみました。
結果としては昔Koaで見たのと同じような動作でした。
ミドルウェアの前処理や後処理の実施順を利用して、タイマーを実装したりして、利用法の検討もしてみました。
Laravelのミドルウェアはartisanと組み合わせて実装が非常に簡単なので、お気軽に使ってみるといいと思います。

今回はこんなところで失礼します。

参考

LaravelのHTTPミドルウェア
Laravel 5.2 追加機能とかメモ
KoaでWeb開発: Koaの検証を通じてミドルウェアの概念を理解する

この投稿は Laravel Advent Calendar 20164日目の記事です。