🔎

Laravel ややマニア向け機能 Controller middleware without resolving ... を試す

2022/10/19に公開

前書き

Laravel 9.35 で実装された「Controller middleware without resolving controller」という機能を試してみたいと思います。

今回の話は、ややマニア向けというか、この機能で恩恵を受ける事は、そう多くは無いと思われます。(機能という表現も微妙かも)

https://github.com/laravel/framework/pull/44516

ちなみに今回のプルリク、かなり色んな議論(すったもんだ(死語?))があったあげく、ようやく最後はシンプルな形に落ち着きました。(Ver.9.63で少しだけ調整されました)

そもそもこれ何?

Laravel のライフサイクルにも関わっていますので、軽く復習から。
Laravel には、ミドルウェアという機能があって、このミドルウェアは、コントローラに向かって実行されるミドルウェア(Before ミドルウェア)と、コントローラから出て行く時に実行されるミドルウェア(After ミドルウェア)があったりします。

いずれにしても、コントローラを実行する前に、「どのようなミドルウェアを実行する必要があるか」の情報収集をする必要があります。ざっくり、下記みたいな流れです。

(1) ミドルウェアの情報収集
(2) ミドルウェアの実行(Before ミドルウェア)
(3) コントローラの実行
(4) 以下、略

で、Laravel では、コントローラのコンストラクタでもミドルウェアが定義できるようになっていますね。

Before ミドルウェアの最終目的地は、コントローラなのに、そのコントローラの中にもミドルウェアの定義が書ける訳ですよ。言ってしまえば、鍵の閉まった宝箱の中に、宝箱の鍵が入っているイメージですね🤔

で、Laravel では、コントローラのコンストラクタに定義されたミドルウェアの情報も収集する為にミドルウェアの情報収集時にコントローラ(のクラス)をインスタンス化することで、コンストラクタを呼び出して情報収集します。

言ってしまえば、宝箱を無理矢理少しこじ開けて、鍵を抜き取るイメージですね。ですので、コントローラのコンストラクタは、ミドルウェアが実行される前に実行されてしまうという、やや切ない感じになっています。

ですが、それだと場合によっては困る事もある為、今回の機能が実装されたという感じになります。

本題

まずは、動作確認用にミドルウェアとコントローラを作成します。

php artisan make:middleware FooMiddleware
php artisan make:controller IndexController

web.phpは、以下。

web.php
use App\Http\Controllers\IndexController;
use App\Http\Middleware\FooMiddleware;

Route::get('/', [IndexController::class, 'index'])
    ->middleware(FooMiddleware::class);

FooMiddleware.php は、以下。

FooMiddleware
class FooMiddleware
{
    public function handle(Request $request, Closure $next)
    {
        dump('FooMiddleware です');

        return $next($request);
    }
}

IndexController.php は、以下。

IndexController
class IndexController extends Controller
{
    public function __construct()
    {
        dump('コントローラの __construct です');
    }

    public function index()
    {
        return view('welcome');
    }
}

上記では、まだ今回の新機能を使用していません。
この状態で、トップページにアクセスしますと、以下のように出力されます。

"コントローラの __construct です"
"FooMiddleware です"

コントローラのコンストラクタが先に実行されている事が分かります。

では、今回の新機能を使って、コントローラのコンストラクタを「ミドルウェアの情報収集時」に実行されないように変更してみましょう。やり方は、2通り提供されています。

(1) 親クラスの Controller を継承しない方法

つまり、コントローラを下記のように変更します。

IndexController
- class IndexController extends Controller
+ class IndexController
{
 
}

すると、今度は出力が逆転します。

"FooMiddleware です"
"コントローラの __construct です"

コントローラのコンストラクタが、ミドルウェアが実行された後に呼び出されるようになりました。

これ、どういう事かというと、Laravel に対して、「私、コントローラでミドルウェア定義しませんから!」と合図を送っているので、Laravel は、ミドルウェアの情報収集時にコントローラのコンストラクタを呼び出さなかったのですね。

(補足:ミドルウエアを定義する為の ->middleware() などのメソッドは、親(先祖)クラスにあり、それを継承しない(or 備えていない)という事は、ミドルウェアの定義を放棄していると解釈できる為)

ちなみに、親クラスを継承する部分を元に戻し、下記のように書いたらどうでしょうか。

IndexController
class IndexController extends Controller
{
    public function __construct()
    {
+        $this->middleware(function ($request, $next) {
+            dump('コントローラの ミドルウェアです');
+
+            return $next($request);
+        });
+
        dump('コントローラの __construct です');
    }

    public function index()
    {
        return view('welcome');
    }
}

これの出力結果は、下記になります。

"コントローラの __construct です"
"FooMiddleware です"
"コントローラの ミドルウェアです"

コントローラで定義したミドルウェアは、最後に実行されていますね。

(2) HasMiddleware インターフェースを実装する方法

(1) では、ミドルウェアの定義を放棄する合図を送ることで、実現できました。

では、「コントローラー内でミドルウェアも定義したいし、コンストラクタもミドルウェアの情報収集時に呼び出されたくない!」という贅沢な願いがあったらどうでしょうか。

その場合、こちらの方法が使えます。HasMiddleware インターフェースを実装し、static な middleware メソッドを定義します。

直前に見た IndexController を書き換えてみます。

IndexController
+ use Illuminate\Routing\Controllers\HasMiddleware;
+ use Illuminate\Routing\Controllers\Middleware;

- class IndexController extends Controller
+ class IndexController implements HasMiddleware
{
    public function __construct()
    {
        dump('コントローラの __construct です');
    }
+
+    public static function middleware()
+    {
+        return [new Middleware(function ($request, $next) {
+            dump('static で定義したミドルウェア');
+
+            return $next($request);
+        })];
+    }

    public function index()
    {
        return view('welcome');
    }
}

出力結果は、以下になります。

"FooMiddleware です"
"static で定義したミドルウェア"
"コントローラの __construct です"

これで無事、実現できました。

おまけ

古い話ではありますが、今回のこの機能を使うと、Laravel 5.3以降、無効になっていた下記のような書き方も、やろうと思えばできてしまう訳ですね。

    public $user;

    public function __construct()
    {
        $this->user = Auth::user();
    }

参考:ララジャパン Laravel 5.3 コントローラのコンストラクタの重要な変更

雑感

という事で、ややマニアックな話でしたが以上になります。

間違い等ありましたら、コメント下さい。

Discussion