【Laravel12】ログイン認証だけで満足しない！'auth'ミドルウェアでゲストユーザの遷移先を制御しよう

## はじめに
### Laravelのバージョン
本記事では、**Laravel12**での開発を前提としています。
構造的にはLaravel11と同じはずで、11.xか12.xであれば動くとは思うのですが、**バージョンが異なる場合は動作保証ができません**。バージョンが異なる環境の場合は、あくまでも参考程度に捉えてください。

### ログイン処理の実装
本記事では、**ログイン処理ができるようになった、その先のこと**について触れます。つまり、ログインができる状態でないと、あんまり意味がありません。

さらに、**複数のログイン認証モデルがある**ことで、この記事は本領を発揮します。例えば、「顧客ユーザと管理者ユーザでそれぞれログインできるようにしたい！」という場合です。

**認証モデルが１つの場合は、デフォルトで用意されている機能で十分です**。

また、認証モデルを新たに作成したい場合は、以下の記事でその方法を説明しております。「自作モデルでのログイン認証実装方法が分からない」という方は、そちらを先にお読みください。そのうえで、**自作モデルでのログイン機能実装を先に済ませることをオススメします**。

https://qiita.com/kinoha/items/932f5cea19cea7452c05

### 本記事における仮のモデル等
本記事では、以下のモデル・ガードで説明を進めます。
- `Customer`モデル：顧客モデル。`Customer`と出されたらコレ
- `customers`ガード：顧客用ガード。`customers`と出されたらコレ
- `Admin`モデル：管理者モデル。`Admin`と出されたらコレ
- `admins`ガード：管理者用ガード。`admins`と出されたらコレ

モデルは、DBなどに登録されるデータとか実体。
ガードとは「そのモデルでログインしてるか？」を確認するための受付とか門。
本記事においては、これくらいの大雑把な認識で良いと思います。~~本記事をお読みになるのであれば、恐らく分かっていると思いますが~~

## 目的
**デフォルトの`auth`ミドルウェアエイリアスを使いつつ、ゲストユーザの遷移先をガード毎に変えること**

本記事では、「ログイン用に作成したガード」に応じて、ゲストユーザが保護されたルートへアクセスしたときの遷移先を変える方法を説明します。特に、**ルート保護の時に使っていた`'auth'`というエイリアスを引き続き使っていける**ようにしていきます。

## 実装
### ルーティング
各モデル向けに名前を付けながらルーティングしていきます。ここでの命名規則は以下の通りです。
- `Customer`関連ルート：`customer.~`
- `Admin`関連ルート：`admin.~`

特に`login`名前付きルートなど**名前の被りやすいルートに関しては、命名規則などを使ってユニークなものにしてください**。

```php:web.php
/* customer向けのルーティング */
Route::get('/customer/login', function () {
    return view('customer.login');
})name('customer.login');

Route::post('/customer/login', [CustomerLoginController::class, 'authenticate']);

Route::get('/customer/dashboard', function () {
    return view('customer.dashboard');
})->middleware('auth:customer')->name('customer.dashboard');

/* admin向けのルーティング */
Route::get('/admin/login', function () {
    return view('admin.login');
})->name('admin.login');

Route::post('/admin/login', [AdminLoginController::class, 'authenticate']);

Route::get('/admin/dashboard', function () {
    return view('admin.dashboard');
})->middleware('auth:admin')->name('admin.dashboard');
```

### ミドルウェアのクラス拡張
#### 指針
**`Illuminate\Auth\Middleware\Authenticate`を拡張するクラスを作成します**。
さらに、そのクラスの中で、**`redirectTo`メソッドをオーバーライドします**。

#### ミドルウェア実装
まずは、ミドルウェアファイルを作成します。ミドルウェア名は何でも構いません。
```bash:bash
php artisan make:middleware Authenticate
```

`redirectTo`メソッドをオーバーライドすることを主として、クラスを作成します。
指針としては、**入力されているガードに対して`if`文や`switch`文などで条件分岐をかける**ことになります。

```php:App\Http\Middleware\Authenticate.php
use Illuminate\Auth\Middleware\Authenticate as Middleware; //追記
use Illuminate\Support\Arr; //追記

class Authenticate extends Middleware
{
    protected $guards;

    public function hundle($request, Closure $next, ...$guards)
    {
        $this->guards = $guards;

        return parent::hundle($request, $next, ...$guards);
    }

    protected function redirectTo(Request $request)
    {
        if($request->expectsJson()) return null;
        switch(Arr::last($this->guards)){
            case "admin":
                return route("admin.login");
                break;
            case "customer":
                return route("customer.login");
                break;
            default:
                return route("customer.login");
                break
        }
    }
}
```

#### ミドルウェアエイリアスの上書き
ルート保護に使う`'auth:~'`というエイリアスが、拡張した`Authenticate`ミドルウェアとなるように設定を上書きします。
この設定は、`bootstrap\app.php`で行います。
`->Middleware(function (Middleware $middleware) { ... }`のコールバック関数に記述をしていきます。
```php:bootstrap\app.php
use App\Http\Middleware\Authenticate; // インポートとして追記

->Middleware(function (Middleware $middleware) {
    $middleware->alias([
        "auth" => Authenticate::class
    ]);
})
```

## 補足説明
### `'auth'`エイリアスによるルート保護
- `middleware('auth')`：デフォルトのガードを利用する
- `middleware('auth:~')`：「~」というガードを利用する
- `middleware(['auth:~', 'auth:='])`：「~」のガードを検証し、通過すれば「=」のガードを検証する（＝どちらも通過しなければならない）
- `middleware('auth:~,=')`：「~」か「=」のどちらかのガードを通るか、その順で検証する（＝どちらかが通過すれば良い）

**後半２つの書き方では挙動が異なります**。正直、「どちらかを通過していれば良い」という要件をいつ使うのかは、開発初心者の私には見当がつきません。コメント等で教えていただけますと嬉しいです。
どちらにしても、**先に記述されたガードが、先に検証されます**。どのガードを優先するのか、どのガードで検証落ちしたらどの画面へ遷移させるのか、等の要件を整えてから実装しましょう。

### `redirectTo`メソッドによる遷移先の選択
好きなように条件やその分岐、遷移先を記述しましょう。
今回は`Arr::last`メソッドを用いて「どのガードに対しても検証落ちしたら、その検証群のうち最後に書かれたガード」を取り出して、条件分岐させました。ここで、「検証群」とは、`'auth:a,b'`のように「複数のガードのうち、どれかを通るか検証する」ものとします。
- 動作例
  - `middleware('auth:a,b')`で、`a`，`b`どちらも検証落ち→`b`のログインページへ
  - `middleware(['auth:a', 'auth:b'])`で、どちらも検証落ち→`a`のログインページへ

例えば、「検証群のうち最も最初に書かれた（優先度の高い）ガード」を取り出したい場合は`Arr::first`メソッドを利用すれば良いです。

また、絶対に配列要素を参照してリダイレクトしなければいけないという制約もありません。容赦なくルートページや404ページに飛ばすとかしても良いです。

### ミドルウェアのデフォルトエイリアスの上書き
Laravel12では、いくつかのミドルウェアのエイリアスが、デフォルトで設定されています。

https://laravel.com/docs/12.x/middleware#middleware-aliases

「これを上書きしてエラー吐かないのか」という話ですが、吐きません。動きます。

内部処理を見てみると、なんやかんやあって、以下のコードでエイリアスが格納されています。
```php
public function getMiddlewareAliases()
{
    return array_merge($this->defaultAliases(), $this->customAliases);
}
```
`defaultAliases()`では、デフォルトエイリアスが記述された連想配列を読み込みます。`customAliases`には、`bootstrap\app.php`で連想配列として記述した連想配列が入っています。
これらをマージして、ミドルウェアエイリアスが形成されるわけですね。

では、`array_merge`メソッドの挙動について、[PHP公式ドキュメント](https://www.php.net/manual/ja/function.array-merge.php)を見てみましょう。

>入力配列が同じキー文字列を有していた場合、そのキーに関する後に指定された値が、 前の値を上書きします。

つまり、今回のように「`'auth'`エイリアスを、別のミドルウェアとして同名で上書きする」みたいなことが許容されるのです。
もし必要があれば、メールアドレス認証用のエイリアス`'verify'`とかも、同じエイリアス名で上書きできる、ということです。

### おまけ：ガード毎にミドルウェアを作る
今回は、主として`redirectTo`メソッドのみを弄って「ガード毎にゲストユーザの遷移先を変える」という機能を実装しました。
これは、**１つのミドルウェアを使って複数ガードのゲスト遷移先を制御する**という設計に基づいています。認証周りを全部`'auth:~'`で統一するには、確かにベストプラクティスかもしれませんね。

ただ、私は先述の通り、「どれかのガードを通ればOKって、どんな設計？」と感じています。ならば、いっそガード毎にミドルウェアを作ったほうが、責務分離がされるのではないかと考えました。

もちろん、これは可能です。指針は以下の通りです。
- `authenticate`メソッドをオーバーライド
ログインしているかの検証を、対象となるガードのみにします。
- `redirectTo`メソッドをオーバーライド
ページ遷移先をただ指定します。

最終的に以下のようになります。
```php:SomeGuardIsAuthenticated.php
use Illuminate\Auth\Middleware\Authenticate;

class SomeGuardIsAuthenticated extends Authenticate
{
    public function ($request, $guards)
    {
        if($this->auth->guard('guard')->check()){
            return $this->auth->shouldUse('guard');
        }

        $this->unauthenticated($request, $guards);
    }

    public function redirectTo($request, $guards)
    {
        if($request->expectsJson()) return null;
        return route('guard.login');
    }
}
```
```php:routes/web.php
Route::get('hoge', function () {...})
->middleware(SomeGuardIsAuthenticated::class); // ミドルウェアの適用
```

メインで説明していた方法を知ると、わざわざこれをする必要はないように思います。chatGPT君に使い道を聞いてみても、「責務分離がはっきりする分、テストや拡張がしやすい」としか、直接的に受けられる利点はなさそうです。

特定のガードに関して、著しく複雑な検証を行うとか、多要素を用いて検証を行うとかをするのであれば、こちらの方がすっきりすると思います。
要件や実装規模に合わせて、デフォルトエイリアスを引き継ぐか、専用ミドルウェアクラスを実装するかを検討しましょう。

## 最後に
本記事では、ミドルウェアを拡張してゲストのリダイレクト先を制御する方法を説明しました。

ここら辺のお話は、chatGPT君に聞くと微妙に信じきれないレベルで返ってきます。Kernel.phpは11.xで廃止されているし、ミドルウェアの設定方法も若干変わっています。
「調べても生成AIに聞いてもできなかった」という方の助けになれれば幸いです。

## 参考
https://zenn.dev/qljmssqh/articles/537f65ff1b1579

https://yashkumarprasad14.medium.com/redirecting-to-different-pages-based-on-authentication-guards-in-laravel-middleware-38118eb67fe0