Laravel11でマルチログイン機能を実装してみた

# はじめに
この記事は、Laravel11でマルチログイン機能を実装する方法について書いています。ぱぱっと実装したい方向けに、細かい説明は省いています。

実装内容について詳しく知りたい場合は、以下を参考にしていただくと、より理解が深まると思います。

https://zenn.dev/seiya0/articles/tech-laravel-multi-login

https://readouble.com/laravel/11.x/ja/authentication.html

# 前提
Laravel11の環境構築が完了している方を対象としています。

# マルチログインの実装
新たにadminsテーブルを作成して、管理者用のログイン機能を実装します。

下記の順に作業を進めていきます。
1. 管理者テーブルとモデルを作成
1. ガード・プロバイダを設定
1. コントローラーを作成
1. ルーティングを設定
1. Bladeファイルを作成
1. ログイン状態、未ログイン状態の場合のリダイレクト先を設定

## 管理者テーブルとモデルを作成
次のコマンドを実行して、マイグレーションファイルとモデルファイルを作成します。
```sh
php artisan make:model -m Admin
```
マイグレーションファイルを修正します。
```diff_php
<?php

use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;

return new class extends Migration
{
    /**
     * Run the migrations.
     */
    public function up(): void
    {
        Schema::create('admins', function (Blueprint $table) {
            $table->id();
+            $table->string('email')->unique();
+            $table->string('password');
            $table->timestamps();
        });
    }

    /**
     * Reverse the migrations.
     */
    public function down(): void
    {
        Schema::dropIfExists('admins');
    }
};
```
マイグレーションを実行します。
```sh
php artisan migrate
```

モデルファイルを修正します。
```diff_php:app/Models/Admin.php
<?php

namespace App\Models;

use Illuminate\Database\Eloquent\Factories\HasFactory;
-use Illuminate\Database\Eloquent\Model;
+use Illuminate\Foundation\Auth\User as Authenticatable;

-class Admin extends Model
+class Admin extends Authenticatable
{
    use HasFactory;
+    
+    protected $fillable = [
+        'email',
+        'password',
+    ];
}
```

## ガード・プロバイダを設定
管理者用のガードとプロバイダを追加します。
コメント部分は省略しています。
```diff_php:config/auth.php
<?php
return [
    'defaults' => [
        'guard' => env('AUTH_GUARD', 'web'),
        'passwords' => env('AUTH_PASSWORD_BROKER', 'users'),
    ],

    'guards' => [
        'web' => [
            'driver' => 'session',
            'provider' => 'users',
        ],
+        'admin' => [
+            'driver' => 'session',
+            'provider' => 'admins',
+        ],
    ],

    'providers' => [
        'users' => [
            'driver' => 'eloquent',
            'model' => env('AUTH_MODEL', App\Models\User::class),
        ],
+       'admins' => [
+            'driver' => 'eloquent',
+            'model' => App\Models\Admin::class,
+        ],

        // 'users' => [
        //     'driver' => 'database',
        //     'table' => 'users',
        // ],
    ],

    'passwords' => [
        'users' => [
            'provider' => 'users',
            'table' => env('AUTH_PASSWORD_RESET_TOKEN_TABLE', 'password_reset_tokens'),
            'expire' => 60,
            'throttle' => 60,
        ],
    ],

    // 略

    'password_timeout' => env('AUTH_PASSWORD_TIMEOUT', 10800),

];

```

## コントローラーを作成
コントローラーを作成します。
```sh
php artisan make:controller AdminLoginController
```
作成したファイルを修正します。
```diff_php:app/Http/Controllers/AdminLoginController.php
<?php

namespace App\Http\Controllers;

use Illuminate\Http\Request;
+use Illuminate\Support\Facades\Auth;

class AdminLoginController extends Controller
{
-   //
+   public function create()
+   {
+        return view('admin.login');
+    }
+
+    public function store(Request $request)
+    {
+        $credentials = $request->validate([
+            'email' => ['required', 'email'],
+            'password' => ['required'],
+        ]);
+
+        if (Auth::guard('admin')->attempt($credentials)) {
+            $request->session()->regenerate();
+
+            return redirect()->intended('/admin/top')->with([
+                'message' => 'ログインしました'
+           ]);
+        }
+
+        return back()->withErrors([
+            'message' => 'メールアドレスかパスワードが間違っています',
+        ])->onlyInput('email');
+    }
+
+    public function destroy(Request $request)
+    {
+        Auth::guard('admin')->logout();
+
+        $request->session()->invalidate();
+
+        $request->session()->regenerateToken();
+
+        return to_route('admin.login');
+    }
}
```

## ルーティングを設定
各機能のルーティングを設定します。
```diff_php:routes/web.php
<?php

use Illuminate\Support\Facades\Route;
+use App\Http\Controllers\AdminLoginController;

Route::get('/', function () {
    return view('welcome');
});

+Route::prefix('admin')->name('admin.')->group(function () {
+    Route::middleware('guest:admin')->group(function() {
+        Route::get('/login', [AdminLoginController::class, 'create'])->name('login');
+        Route::post('/login', [AdminLoginController::class, 'store']);
+    });
+   Route::middleware('auth:admin')->group(function() {
+        Route::get('/top', function() {
+            return view('/admin/top');
+        })->name('top');
+    });
+});
```

## Bladeファイルを作成
トップ画面とログイン画面のBladeファイルを作成します。ログイン後はトップ画面にリダイレクトさせます。
```sh
php artisan make:view admin/top
php artisan make:view admin/login
```

トップ画面のBladeファイルを修正します。
```diff_php:views/admin/top.blade.php
-<div>
-    <!-- Walk as if you are kissing the Earth with your feet. - Thich Nhat Hanh -->
-</div>
+<head>
+    <style>
+        dialog {
+            border: none;
+            outline: none;
+            border-radius: 10px;
+            box-shadow: 0 4px 15px rgba(0, 0, 0, 0.2);
+            padding: 5px 20px;
+            font-family: Arial, sans-serif;
+        }
+    </style>
+</head>
+<div>
+    @session('message')
+        <dialog id="dialog">
+            <p>{{ session('message') }}</p>
+        </dialog>
+    @endsession
+    <h1>トップ画面</h1>
+</div>
+<script>
+    const dialog = document.getElementById('dialog');
+    dialog.show();
+
+    setTimeout(() => {
+        dialog.close();
+    }, 3000);
+</script>

```

login画面のBladeファイルを修正します。
```diff_php:views/admin/login.blade.php
-<div>
-    <!-- You must be the change you wish to see in the world. - Mahatma Gandhi -->
-</div>
+<div>
+     <form method="POST" action="/admin/login">
+        @csrf
+        @error('message')
+            <div class="alert alert-danger">{{ $message }}</div>
+        @enderror
+        <div>
+            <label for="email">メールアドレス</label>
+            <input type="text" name="email" id="email" required />
+            @error('email')
+                <div class="alert alert-danger">{{ $message }}</div>
+            @enderror
+        </div>
+        <div>
+            <label for="password">パスワード</label>
+            <input type="text" name="password" id="password" required />
+            @error('password')
+                <div class="alert alert-danger">{{ $message }}</div>
+            @enderror
+        </div>
+        <div>
+            <button type="submit">ログイン</button>
+        </div>
+    </form>
+</div>
```

## ログイン状態、非ログイン状態でのリダイレクト先を設定
最後に、リダイレクトの設定をします。
```diff_php:bootstrap/app.php
<?php

use Illuminate\Foundation\Application;
use Illuminate\Foundation\Configuration\Exceptions;
use Illuminate\Foundation\Configuration\Middleware;
+use Illuminate\Http\Request;

return Application::configure(basePath: dirname(__DIR__))
    ->withRouting(
        web: __DIR__.'/../routes/web.php',
        commands: __DIR__.'/../routes/console.php',
        health: '/up',
    )
    ->withMiddleware(function (Middleware $middleware) {
-        //
+        // 非ログインユーザー用のリダイレクト設定
+        $middleware->redirectGuestsTo(function (Request $request) {
+            if (request()->routeIs('admin.*')) {
+                return $request->expectsJson() ? null : route('admin.login');
+            }
+        });
+
+        // ログインユーザー用のリダイレクト設定
+        $middleware->redirectUsersTo(function () {
+            if(Auth::guard('admin')) {
+                return route('admin.top');
+            }
+        });
    })
    ->withExceptions(function (Exceptions $exceptions) {
        //
    })->create();

```

以上で実装は完了です。
adminsテーブルに適当なデータを挿入してから、動作確認をしてみてください。

# 参考
https://zenn.dev/seiya0/articles/tech-laravel-multi-login

https://readouble.com/laravel/11.x/ja/authentication.html