Laravel 終了処理ミドルウェア(Terminable Middleware)で遊んで見る
はじめに
あまり語られる事のない、終了処理ミドルウェア(Terminable Middleware)で遊んで見ました。
これはどんな時に使えるかというと、例えばちょっとしたログなんか取ったりする際に使えたりします(重要なログは、これではなく、要所要所で取るべきだと思いますが)。若しくは、文字通り何か終了処理をさせたいものがある場合にも使えます。
で、このミドルウェアの何がいいかと言うと、レスポンスを送った後に処理をするので、その分、ユーザーの待ち時間が少なくなります。但し、FastCGIを使っている場合に限ります。
普通のミドルウェアとどう違うの?
普通の before や after のミドルウェアとは、呼ばれるタイミングが異なります。
after より後で、レスポンスを送った後に呼ばれます。
ざっくり仕組みは?
終了処理ミドルウェアは、public/index.php の一番下の箇所から呼び出されます。
// 上部は略
$kernel = $app->make(Illuminate\Contracts\Http\Kernel::class);
$response = $kernel->handle(
$request = Illuminate\Http\Request::capture()
);
$response->send();
$kernel->terminate($request, $response);
Ver.8以降では、ちょっとだけ記述が小難しくなっている為、Ver.7のを記述しています。
$kernel->terminate(...) の箇所から Illuminate\Foundation\Http\Kernel の terminate() メソッドが呼び出され、終了処理ミドルウェアが実行されます。
その1つ手前で、$response->send(); してますね。これは、Symfony の Symfony\Component\HttpFoundation\Response の send() を呼び出します。
以下のようになっています。
public function send()
{
$this->sendHeaders();
$this->sendContent();
if (\function_exists('fastcgi_finish_request')) {
fastcgi_finish_request();
} elseif (!\in_array(\PHP_SAPI, ['cli', 'phpdbg'], true)) {
static::closeOutputBuffers(0, true);
}
return $this;
}
fastcgi_finish_request(); が呼び出される事で、ユーザーを待たせず、終了処理を行う事ができる訳ですね。FastCGI と言うだけあって、まさに Fast です。
と言うことで、少し遊んで見る
今回は、会員サイトで会員が何か行った際に簡易ログを取る、という想定で進めて行きます。
まずは、ミドルウェアを作成します。以下のコマンドを打ちます。
php artisan make:middleware LogAction
できた LogAction に以下を追加します。
public function terminate($request, $response)
{
if ($request->isMethod('GET')) {
return;
}
$id = $request->user()->id;
sleep(3);
$data = [
'method' => $request->method(), // 'POST', 'DELETE' など
'url' => $request->url(),
'input' => $request->input(), // ->all() ではないのでファイル系は除かれる
];
info("User ID {$id}", $data); // ログを取る
}
まずログは、GET以外の時に取るようにしています。
続けて、会員の連番IDを取得し、検証用に3秒sleepします。
その後、ちょっとした情報をログに取ります。
info() ヘルパー関数を使って、デフォルトのログチャンネルにinfoレベルのログとしていますが、本来であれば、ログチャンネルは分けた方が良いですね。
このミドルウェアを使用する為に、web.php に例えば以下のように記述します。
use App\Http\Middleware\LogAction;
Route::middleware(['auth', LogAction::class])->group(function () {
// 会員用のルーティング設定をこの中に
});
ミドルウェアは、Http\Kernel にニックネームを登録してやるのが一般的かと思いますが、Laravel8以降、コントローラーもフルパスで書く時代ですので、ミドルウェアもフルパスで書くようにしています。(その方が対象ファイルにすぐ飛べます)
これで、会員サイトで何か情報を登録・更新・削除する度ごとに、ログが取られます。
丁度私のWEBサーバは、FastCGIを使っているので、3秒待たされずに画面が表示され、3秒後位にログが取られます。
ユーザーを待たせないというのはいいですね。但し、イケメン FastCGIを使っている場合に限ります。
ちなみに、FastCGI でなかったとした場合の環境を試すため、先程の send() メソッドの fastcgi_finish_request(); 周りをカットして、以下のようにすると、
public function send()
{
$this->sendHeaders();
$this->sendContent();
if (!\in_array(\PHP_SAPI, ['cli', 'phpdbg'], true)) {
static::closeOutputBuffers(0, true);
}
return $this;
}
3秒程待たされてから画面が表示され、それと同時にログが取られます。
なんかいい感じだけど、注意点とかは?
基本的に、処理が前後するとバグになるような場合は、これは使うべきでは無いです。
昔のLaravelでは、セッションを保存するのに、この terminate() を使っていましたが、セッションをDBに保存するようにした場合で、DBに高負荷が掛かっていて処理が遅い場合、セッションがDBに保存される前に、次のリクエストがやってきてしまい、不具合が生じるという事がありました。
なので今は、terminate() ではなく、普通の after ミドルウェアに変更されています。
参考:GitHub [5.8] Fixes session attributes persistence
まとめ
この機能、いつお世話になる事やら…。
おかしな箇所等ありましたら、コメント下さい。
Discussion