Laravel Breeze: Multi Auth しながらログイン・登録・パスワード再発行・Email Verification に対応させる

こんにちは❗フリーランス・コンサルタント&エンジニアの 九保すこひ です。

さてさて、Laravel開発をしたことがある人なら誰でも聞いたことがあるパッケージが「Laravel Breeze」じゃないでしょうか。

これは以下のような機能を一手に引き受けてくれるスグレモノで、私もよくお世話になっています。

  • ログイン
  • ユーザー登録
  • パスワード再発行
  • 本登録用のURLをメール送信

などなど。

しかし、実はこのLaravel Breezeuserという名前のログインタイプだけでなく、拡張すれば別のものも利用できるのをご存知でしょうか。

例えば・・・・・・

  • student
  • teacher
  • parent

※ この機能はMulti Auth(まるち・おーす)と呼ばれる機能です。

そして、Multi Authを利用することでDBテーブルを分けることができ、各ユーザーの登録内容が違う場合などにとても便利です。

しかし❗

実は、ログインだけならそれほど難しくはないのですが、全ての機能をMulti Auth化するとなかなかやっかいだったりします。

そこで❗

今回は今後の自分のための備忘録としてもLaravel BreezeMulti Authを使う方法を記事としてまとめました。

ぜひ何かのお役に立てば嬉しいです。😄✨

※ ちなみに今回の記事はホントにたくさんの箇所で作成&変更が必要で私自身コンガラがってしまいました。。もしどこか抜けている部分があったらぜひ教えてください。🙏✨

「ちょっと記事が長くなるかな…
が想定の3倍ぐらいになってしまいました(笑)」

開発環境: Laravel 10.x(今回はフロントエンドは使用せず、Bladeを使ってます)

やりたいこと

今回は学生さんたちがログインする「student」というログインタイプ(guard)を作ってみます。

つまり、モデルはStudent.phpになりますし、URLも/studentで始まります。

また、今回は「student」だけですが、もし「teacher」や「seller」などその他のタイプを追加する場合は同じ作業をすればOKです。

ただし、今回はログイン後の部分は省略しています。
あまりにも長い記事になりすぎたので…💦

※ なお、少し悩みましたがコントローラーやモデルの共通化はしないことにしました。というのも、ログインするユーザータイプによって例外が発生することが想定されるからです。(もし共通化したい場合はBase******Controllerなどをつくり、それを継承してそれぞれのコントローラーをつくるといいでしょう)

では、楽しんでやっていきましょう❗

Laravel Breeze をインストールする

まずはログイン機能がLaravelで使えないと始まりませんので、Laravel Breezeをインストールします。

以下のコマンドを実行してください。

composer require laravel/breeze --dev

すると、パッケージがインストールされるので、続けて以下のコマンドを実行します。

php artisan breeze:install blade
php artisan migrate

※ ちなみに、今回はVueなどのフロントエンドは使わずBladeのみで実装しますが、基本的には同じです。(ビューとかは変更してください)

これで、DBにテーブルが作成されログイン機能がインストールされました。

ヘルパー関数をつくる

では、ここからがMulti Authの作業にはいりますが、まずはどこからでも「今どのログインタイプかが分かる」ようにヘルパー関数を作っておきます。

この関数は現在のURLからキーワードを切り出し、例えば「student」などを返します。

app/helpers.php

<?php

use Illuminate\Support\Str;

if (! function_exists('multi_auth_guard')) {

    function multi_auth_guard(): ?string
    {
        $current_uri = request()->path();
        $guard_types = ['student']; // 👈 複数ある場合はここに追加する

        foreach ($guard_types as $guard_type) {

            if(Str::startsWith($current_uri, $guard_type)) {

                return $guard_type;

            }

        }

        return null;
    }

}

※ なお、この中にある$guard_typesに含まれているものがMulti Authの種類(guard)になります。もしstudentの他にも増やしたい場合は以下のように変更してください。

$guard_types = ['student', 'teacher', 'parent'];
そして、ファイルは作成しただけでは有効にはなりませんので、Laravelへ登録します。

composer.json

"autoload": {
    "files": [
        "app/helpers.php"
    ],
    "psr-4": {
        "App\\": "app/",
        "Database\\Factories\\": "database/factories/",
        "Database\\Seeders\\": "database/seeders/"
    }
},

filesの部分を追加しました。

では、登録できたら以下のコマンドで実際に有効にします。

composer dump-autoload

これで、helpers.phpがどこにあるかがLaravelに分かるようになり、登録されたヘルパー関数がどこからでも実行できるようになります。

View composerをつくる

View composerとは、シンプルに言うと「先に値をセットしておけば、どのビューからでも取り出せるよ👍」機能のことです。

つまり、いちいち以下のようにしなくてもよくなるというスグレモノです。

return view('home', [
    'test' => 'テスト文字列', // 👈 毎回セットするのがメンドウ!
]);

そして、セットする値は先ほどヘルパー関数の中身(ログインタイプ)です。
では、次のファイルを作成してください。

app/Providers/ViewServiceProvider.php

<?php

namespace App\Providers;

use Illuminate\Support\Facades\View;
use Illuminate\Support\ServiceProvider;

class ViewServiceProvider extends ServiceProvider
{
    /**
     * Register any application services.
     *
     * @return void
     */
    public function register()
    {
        //
    }

    /**
     * Bootstrap any application services.
     *
     * @return void
     */
    public function boot()
    {
        View::composer('*', function ($view) {

            $multi_auth_guard = multi_auth_guard();

            if(! is_null($multi_auth_guard)) {

                $view->with('multi_auth_guard', $multi_auth_guard);

            }

        });
    }
}

そして、このファイルをLaravel側へ登録しておきましょう。

config/app.php

// 省略

/*
 * Package Service Providers...
 */
App\Providers\ViewServiceProvider::class, // 👈 ここを追加しました

// 省略

これで、どのビューからでも$multi_auth_guardにアクセスすることができるようになりました。

guard(ログインタイプ)を設定ファイルに書き込む

次に、これも準備のようなものですがLaravelにログインタイプ(guard)をセットします。

今回はstudentだけですので、以下のようになります。

config/auth.php

// 省略

'guards' => [
    'web' => [
        'driver' => 'session',
        'provider' => 'users',
    ],
    'student' => [ // 👈 ここを追加しました
        'driver' => 'session',
        'provider' => 'students',
    ],
],

// 省略

'providers' => [
    'users' => [
        'driver' => 'eloquent',
        'model' => App\Models\User::class,
    ],
    'students' => [ // 👈 ここを追加しました
        'driver' => 'eloquent',
        'model' => App\Models\Student::class,
    ],
],

// 省略

'passwords' => [
    'users' => [
        'provider' => 'users',
        'table' => 'password_reset_tokens',
        'expire' => 60,
        'throttle' => 60,
    ],
    'students' => [ // 👈 ここを追加しました
        'provider' => 'students',
        'table' => 'password_reset_tokens',
        'expire' => 60,
        'throttle' => 60,
    ],
],

⚠ご注意: ちなみに、書き方は自由ですがLaravelのお作法に沿って書いていますので、「student」と「students」の単数形/複数形が入り混じっています。お気をつけください❗

これで、「student」というログインタイプが追加されました。

モデル・マイグレーション・Factory・Seeder をつくる

では、次にDB周り(モデル・マイグレーション・Factory・Seeder)の作業をしていきます。

以下のコマンドを実行してください。

php artisan make:model Student -msf

すると、4つのファイルが作成されるので、それぞれ中身を変更します。

モデル

app/Models/Student.php

<?php

namespace App\Models;

use App\Notifications\ResetPasswordForMultiAuth; // 👈 追加しました
use App\Notifications\VerifyEmailForMultiAuth;// 👈 追加しました
use Illuminate\Contracts\Auth\MustVerifyEmail; // 👈 コメントアウトを解除しました
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Foundation\Auth\User as Authenticatable;
use Illuminate\Notifications\Notifiable;
use Laravel\Sanctum\HasApiTokens;

class Student extends Authenticatable implements MustVerifyEmail // MustVerifyEmailを追加しました
{
    use HasApiTokens, HasFactory, Notifiable;

    /**
     * The attributes that are mass assignable.
     *
     * @var array<int, string>
     */
    protected $fillable = [
        'name',
        'email',
        'password',
    ];

    /**
     * The attributes that should be hidden for serialization.
     *
     * @var array<int, string>
     */
    protected $hidden = [
        'password',
        'remember_token',
    ];

    /**
     * The attributes that should be cast.
     *
     * @var array<string, string>
     */
    protected $casts = [
        'email_verified_at' => 'datetime',
        'password' => 'hashed',
    ];

    // 👇 以降、オーバーライドしました
    public function sendPasswordResetNotification($token)
    {
        $this->notify(new ResetPasswordForMultiAuth($token));
    }

    public function sendEmailVerificationNotification()
    {
        $this->notify(new VerifyEmailForMultiAuth());
    }
}

マイグレーション

database/migrations/****_**_**_******_create_students_table.php

// 省略

Schema::create('students', function (Blueprint $table) {
    $table->id();
    $table->string('name');
    $table->string('email')->unique();
    $table->timestamp('email_verified_at')->nullable();
    $table->string('password');
    $table->rememberToken();
    $table->timestamps();
});

// 省略

Factory

database/factories/StudentFactory.php

// 省略

public function definition(): array
{
    return [
        'name' => fake()->name(),
        'email' => fake()->unique()->safeEmail(),
        'email_verified_at' => now(),
        'password' => Hash::make('password'),
        'remember_token' => Str::random(10),
    ];
}

// 省略

Seeder

database/seeders/StudentSeeder.php

<?php

namespace Database\Seeders;

use Database\Factories\StudentFactory;
use Illuminate\Database\Console\Seeds\WithoutModelEvents;
use Illuminate\Database\Seeder;

class StudentSeeder extends Seeder
{
    /**
     * Run the database seeds.
     */
    public function run(): void
    {
        for($i = 0; $i < 10; $i++) {

            $email = 'student' . $i . '@example.com';

            StudentFactory::new()->create([
                'email' => $email,
            ]);

        }
    }
}

ちなみに、私の場合すぐ定義場所に移動できなくなるので、

User::factory()->create();

という書き方はしません。(もしかして、こっちのほうが主流なのかな…🤔)

Seederは作っただけでは有効になりませんので、DatabaseSeederへ登録します。

 

database/seeders/DatabaseSeeder.php
<?php

namespace Database\Seeders;

// use Illuminate\Database\Console\Seeds\WithoutModelEvents;
use Illuminate\Database\Seeder;

class DatabaseSeeder extends Seeder
{
    /**
     * Seed the application's database.
     */
    public function run(): void
    {
        // 省略

        $this->call([ // 👈 ここを追加しました
            StudentSeeder::class,
        ]);
    }
}

では、この状態でデータベースを初期化してみましょう。
以下のコマンドを実行してください。

php artisan migrate:fresh --seed

すると、実際のテーブルはこうなりました。

Notification をつくる

先ほど、モデルの中で2つのNotification(≒ メール送信)をセットしましたのでこれらをつくります。

※ ちなみに、初期状態を踏まえてNotificationを使っていますが、個人的にはあまり使い勝手がいいと感じないので、Mailableを使うほうが好みです。HTMLメールに対応していない場合もまれにあるようですしね😉

パスワード再発行の時のメール送信

以下のコマンドを実行してください。

 php artisan make:notification ResetPasswordForMultiAuth

そして、作成されたファイルの中身を次のように変更します。

app/Notifications/ResetPasswordForMultiAuth.php

<?php

namespace App\Notifications;

use Illuminate\Auth\Notifications\ResetPassword;

class ResetPasswordForMultiAuth extends ResetPassword
{
    protected function resetUrl($notifiable)
    {
        if (static::$createUrlCallback) {
            return call_user_func(static::$createUrlCallback, $notifiable, $this->token);
        }

        $multi_auth_guard = multi_auth_guard();
        $route_name = $multi_auth_guard . '.password.reset';

        return url(route($route_name, [
            'token' => $this->token,
            'email' => $notifiable->getEmailForPasswordReset(),
        ], false));
    }
}

※ なお、このNotificationは初期状態で利用されるIlluminate\Auth\Notifications\ResetPasswordを拡張したものです。

Email verification の時のメール送信

次にEmail verification(本登録用のURLを送信する機能)のためのNotificationです。

以下のコマンドを実行してください。

php artisan make:notification VerifyEmailForMultiAuth

ファイルが作成されるので、中身を次のようにします。

<?php

namespace App\Notifications;

use Illuminate\Auth\Notifications\VerifyEmail as BaseVerifyEmail;
use Illuminate\Support\Carbon;
use Illuminate\Support\Facades\Config;
use Illuminate\Support\Facades\URL;

class VerifyEmailForMultiAuth extends BaseVerifyEmail
{
    private $multi_auth_guard;

    public function __construct()
    {
        $this->multi_auth_guard = multi_auth_guard();
    }

    protected function verificationUrl($notifiable)
    {
        if (static::$createUrlCallback) {
            return call_user_func(static::$createUrlCallback, $notifiable);
        }

        $route_name = $this->multi_auth_guard .'.verification.verify';

        return URL::temporarySignedRoute(
            $route_name,
            Carbon::now()->addMinutes(Config::get('auth.verification.expire', 60)),
            [
                'id' => $notifiable->getKey(),
                'hash' => sha1($notifiable->getEmailForVerification()),
            ]
        );
    }
}

※ これも、元々のIlluminate\Auth\Notifications\VerifyEmailを拡張したものです。

コントローラーを拡張する

では、続いて各種コントローラーを作っていきます。

今回はstudentというguardですので、app/Http/Controllers/Studentフォルダへこれらのコントローラーを設置します。

※ なお、できるだけ汎用的につくっているので、ネームスペースを変更するだけでOKだと思います。

ログイン

app/Http/Controllers/Student/AuthenticatedSessionController.php

<?php

namespace App\Http\Controllers\Student;

use App\Http\Controllers\Controller;
use App\Http\Requests\Auth\MultiAuthLoginRequest;
use Illuminate\Http\RedirectResponse;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;
use Illuminate\View\View;

class AuthenticatedSessionController extends Controller
{
    private $multi_auth_guard;

    public function __construct()
    {
        $this->multi_auth_guard = multi_auth_guard();
    }

    /**
     * Display the login view.
     */
    public function create(): View
    {
        return view($this->multi_auth_guard .'.auth.login');
    }

    /**
     * Handle an incoming authentication request.
     */
    public function store(MultiAuthLoginRequest $request): RedirectResponse
    {
        $request->authenticate();

        $request->session()->regenerate();

        $redirect_url = route($this->multi_auth_guard .'.dashboard'); // ログイン後のリダイレクト先

        return redirect()->intended($redirect_url);
    }

    /**
     * Destroy an authenticated session.
     */
    public function destroy(Request $request): RedirectResponse
    {
        Auth::guard($this->multi_auth_guard)->logout();

        $request->session()->invalidate();

        $request->session()->regenerateToken();

        return to_route($this->multi_auth_guard .'.login');
    }
}

ユーザー登録

app/Http/Controllers/Student/RegisteredUserController.php

<?php

namespace App\Http\Controllers\Student;

use App\Http\Controllers\Controller;
use Illuminate\Auth\Events\Registered;
use Illuminate\Http\RedirectResponse;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Hash;
use Illuminate\Validation\Rules;
use Illuminate\View\View;

class RegisteredUserController extends Controller
{
    private $multi_auth_guard;

    public function __construct()
    {
        $this->multi_auth_guard = multi_auth_guard();
    }

    /**
     * Display the registration view.
     */
    public function create(): View
    {
        return view($this->multi_auth_guard .'.auth.register');
    }

    /**
     * Handle an incoming registration request.
     *
     * @throws \Illuminate\Validation\ValidationException
     */
    public function store(Request $request): RedirectResponse
    {
        $model_name = 'App\\Models\\'. ucfirst($this->multi_auth_guard);

        $request->validate([
            'name' => ['required', 'string', 'max:255'],
            'email' => ['required', 'string', 'email', 'max:255', 'unique:'. $model_name],
            'password' => ['required', 'confirmed', Rules\Password::defaults()],
        ]);

        $user = $model_name::create([
            'name' => $request->name,
            'email' => $request->email,
            'password' => Hash::make($request->password),
        ]);

        event(new Registered($user));

        auth($this->multi_auth_guard)->login($user);

        return to_route($this->multi_auth_guard .'.dashboard');
    }
}

パスワード再発行

app/Http/Controllers/Student/PasswordResetLinkController.php

<?php

namespace App\Http\Controllers\Student;

use App\Http\Controllers\Controller;
use Illuminate\Http\RedirectResponse;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Password;
use Illuminate\Support\Str;
use Illuminate\View\View;

class PasswordResetLinkController extends Controller
{
    private $multi_auth_guard;

    public function __construct()
    {
        $this->multi_auth_guard = multi_auth_guard();
    }

    /**
     * Display the password reset link request view.
     */
    public function create(): View
    {
        return view($this->multi_auth_guard .'.auth.forgot-password');
    }

    /**
     * Handle an incoming password reset link request.
     *
     * @throws \Illuminate\Validation\ValidationException
     */
    public function store(Request $request): RedirectResponse
    {
        $request->validate([
            'email' => ['required', 'email'],
        ]);

        $provider_name = Str::plural($this->multi_auth_guard); // 複数形にする

        // We will send the password reset link to this user. Once we have attempted
        // to send the link, we will examine the response then see the message we
        // need to show to the user. Finally, we'll send out a proper response.
        $status = Password::broker($provider_name)->sendResetLink(
            $request->only('email')
        );

        return $status == Password::RESET_LINK_SENT
                    ? back()->with('status', __($status))
                    : back()->withInput($request->only('email'))
                            ->withErrors(['email' => __($status)]);
    }
}

app/Http/Controllers/Student/NewPasswordController.php

<?php

namespace App\Http\Controllers\Student;

use App\Http\Controllers\Controller;
use Illuminate\Auth\Events\PasswordReset;
use Illuminate\Http\RedirectResponse;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Hash;
use Illuminate\Support\Facades\Password;
use Illuminate\Support\Str;
use Illuminate\Validation\Rules;
use Illuminate\View\View;

class NewPasswordController extends Controller
{
    private $multi_auth_guard;

    public function __construct()
    {
        $this->multi_auth_guard = multi_auth_guard();
    }

    /**
     * Display the password reset view.
     */
    public function create(Request $request): View
    {
        return view($this->multi_auth_guard .'.auth.reset-password', ['request' => $request]);
    }

    /**
     * Handle an incoming new password request.
     *
     * @throws \Illuminate\Validation\ValidationException
     */
    public function store(Request $request): RedirectResponse
    {
        $request->validate([
            'token' => ['required'],
            'email' => ['required', 'email'],
            'password' => ['required', 'confirmed', Rules\Password::defaults()],
        ]);

        $provider_name = Str::plural($this->multi_auth_guard);

        // Here we will attempt to reset the user's password. If it is successful we
        // will update the password on an actual user model and persist it to the
        // database. Otherwise we will parse the error and return the response.
        $status = Password::broker($provider_name)->reset(
            $request->only('email', 'password', 'password_confirmation', 'token'),
            function ($user) use ($request) {
                $user->forceFill([
                    'password' => Hash::make($request->password),
                    'remember_token' => Str::random(60),
                ])->save();

                event(new PasswordReset($user));
            }
        );

        $route_name = $this->multi_auth_guard .'.login';

        // If the password was successfully reset, we will redirect the user back to
        // the application's home authenticated view. If there is an error we can
        // redirect them back to where they came from with their error message.
        return $status == Password::PASSWORD_RESET
                    ? redirect()->route($route_name)->with('status', __($status))
                    : back()->withInput($request->only('email'))
                            ->withErrors(['email' => __($status)]);
    }
}

Email verification

Email verification」とは、ユーザー登録するときに一旦メールアドレスに本登録URLを送信し、そのリンクがクリックされたら実際に登録になるという機能です(つまり、ちゃんとメールアドレスが存在しているか確認しているわけですね)

app/Http/Controllers/Student/EmailVerificationNotificationController.php

<?php

namespace App\Http\Controllers\Student;

use App\Http\Controllers\Controller;
use Illuminate\Http\RedirectResponse;
use Illuminate\Http\Request;

class EmailVerificationNotificationController extends Controller
{
    /**
     * Send a new email verification notification.
     */
    public function store(Request $request): RedirectResponse
    {
        $multi_auth_guard = multi_auth_guard();
        $intended_url = route($multi_auth_guard .'.dashboard');

        if ($request->user()->hasVerifiedEmail()) {
            return redirect()->intended($intended_url);
        }

        $request->user()->sendEmailVerificationNotification();

        return back()->with('status', 'verification-link-sent');
    }
}

app/Http/Controllers/Student/EmailVerificationPromptController.php

<?php

namespace App\Http\Controllers\Student;

use App\Http\Controllers\Controller;
use Illuminate\Http\RedirectResponse;
use Illuminate\Http\Request;
use Illuminate\View\View;

class EmailVerificationPromptController extends Controller
{
    /**
     * Display the email verification prompt.
     */
    public function __invoke(Request $request): RedirectResponse|View
    {
        $multi_auth_guard = multi_auth_guard();
        $intended_url = route($multi_auth_guard .'.dashboard');
        $view_name = $multi_auth_guard .'.auth.verify-email';

        return $request->user()->hasVerifiedEmail()
                    ? redirect()->intended($intended_url)
                    : view($view_name);
    }
}

app/Http/Controllers/Student/VerifyEmailController.php

<?php

namespace App\Http\Controllers\Student;

use App\Http\Controllers\Controller;
use Illuminate\Auth\Events\Verified;
use Illuminate\Foundation\Auth\EmailVerificationRequest;
use Illuminate\Http\RedirectResponse;

class VerifyEmailController extends Controller
{
    /**
     * Mark the authenticated user's email address as verified.
     */
    public function __invoke(EmailVerificationRequest $request): RedirectResponse
    {
        $multi_auth_guard = multi_auth_guard();
        $redirect_route_name = $multi_auth_guard .'.dashboard';

        if ($request->user()->hasVerifiedEmail()) {
            return to_route($redirect_route_name);
        }

        if ($request->user()->markEmailAsVerified()) {
            event(new Verified($request->user()));
        }

        return to_route($redirect_route_name);
    }
}

FormRequest をつくる

次に、AuthenticatedSessionController.phpで使ったMultiAuthLoginRequestをつくります。

以下のコマンドを実行してください。

php artisan make:request Auth\\MultiAuthLoginRequest

すると、ファイルが作成されるので中身を次のように変更します。

app/Http/Requests/Auth/MultiAuthLoginRequest.php

<?php

namespace App\Http\Requests\Auth;

use Illuminate\Auth\Events\Lockout;
use Illuminate\Foundation\Http\FormRequest;
use Illuminate\Support\Facades\RateLimiter;
use Illuminate\Support\Str;
use Illuminate\Validation\ValidationException;

class MultiAuthLoginRequest extends FormRequest
{
    /**
     * Determine if the user is authorized to make this request.
     */
    public function authorize(): bool
    {
        return true;
    }

    /**
     * Get the validation rules that apply to the request.
     *
     * @return array<string, \Illuminate\Contracts\Validation\Rule|array|string>
     */
    public function rules(): array
    {
        return [
            'email' => ['required', 'string', 'email'],
            'password' => ['required', 'string'],
        ];
    }

    /**
     * Attempt to authenticate the request's credentials.
     *
     * @throws \Illuminate\Validation\ValidationException
     */
    public function authenticate(): void
    {
        $this->ensureIsNotRateLimited();
        $multi_auth_guard = multi_auth_guard();

        if (! auth($multi_auth_guard)->attempt($this->only('email', 'password'), $this->boolean('remember'))) {
            RateLimiter::hit($this->throttleKey());

            throw ValidationException::withMessages([
                'email' => trans('auth.failed'),
            ]);
        }

        RateLimiter::clear($this->throttleKey());
    }

    /**
     * Ensure the login request is not rate limited.
     *
     * @throws \Illuminate\Validation\ValidationException
     */
    public function ensureIsNotRateLimited(): void
    {
        if (! RateLimiter::tooManyAttempts($this->throttleKey(), 5)) {
            return;
        }

        event(new Lockout($this));

        $seconds = RateLimiter::availableIn($this->throttleKey());

        throw ValidationException::withMessages([
            'email' => trans('auth.throttle', [
                'seconds' => $seconds,
                'minutes' => ceil($seconds / 60),
            ]),
        ]);
    }

    /**
     * Get the rate limiting throttle key for the request.
     */
    public function throttleKey(): string
    {
        return Str::transliterate(Str::lower($this->input('email')).'|'.$this->ip());
    }
}

各種ミドルウェアを作成&変更する

Multi Authを実現するためにはいくつかミドルウェアが必要になってきます。
それぞれ作成&変更してください。

ログイン失敗時のリダイレクト先を変更する

このミドルウェアはすでに存在していますので、中身を以下のように変更します。

app/Http/Middleware/Authenticate.php

<?php

namespace App\Http\Middleware;

use Illuminate\Auth\Middleware\Authenticate as Middleware;
use Illuminate\Http\Request;

class Authenticate extends Middleware
{
    /**
     * Get the path the user should be redirected to when they are not authenticated.
     */
    protected function redirectTo(Request $request): ?string
    {
        if (! $request->expectsJson()) {

            $multi_auth_guard = multi_auth_guard();

            if(! is_null($multi_auth_guard)) {

                return route($multi_auth_guard .'.login');

            }

            return url('/');
        }

        return null;
    }
}

Email Verification が完了しているかチェックするミドルウェア

このミドルウェアは存在していませんので、以下のコマンドで新規作成します。

php artisan make:middleware EnsureEmailIsVerifiedForMultiAuth

すると、ファイルが作成されるので中身を次のようにします。

app/Http/Middleware/EnsureEmailIsVerifiedForMultiAuth.php

<?php

namespace App\Http\Middleware;

use Closure;
use Illuminate\Contracts\Auth\MustVerifyEmail;
use Illuminate\Support\Facades\Redirect;
use Illuminate\Support\Facades\URL;
use Symfony\Component\HttpFoundation\Response;

class EnsureEmailIsVerifiedForMultiAuth
{
    /**
     * Handle an incoming request.
     *
     * @param  \Closure(\Illuminate\Http\Request): (\Symfony\Component\HttpFoundation\Response)  $next
     */
    public function handle($request, Closure $next, $redirectToRoute = null): Response
    {
        $multi_auth_guard = multi_auth_guard();
        $redirect_route_name = $multi_auth_guard .'.verification.notice';

        if (! $request->user() ||
            ($request->user() instanceof MustVerifyEmail &&
                ! $request->user()->hasVerifiedEmail())) {
            return $request->expectsJson()
                ? abort(403, 'Your email address is not verified.')
                : Redirect::guest(URL::route($redirectToRoute ?: $redirect_route_name));
        }

        return $next($request);
    }
}

そして、このミドルウェアをLaravelに登録してください。

app/Http/Kernel.php

<?php

namespace App\Http;

use App\Http\Middleware\EnsureEmailIsVerifiedForMultiAuth; // 👈 ここを追加しました
use Illuminate\Foundation\Http\Kernel as HttpKernel;

class Kernel extends HttpKernel
{

    // 省略

    protected $middlewareAliases = [

        // 省略

        'verified_for_multi_auth' => EnsureEmailIsVerifiedForMultiAuth::class, // 👈 ここを追加しました
    ];
}

すでにログイン済みのときのリダイレクト先を変更する

app/Http/Middleware/RedirectIfAuthenticated.php

<?php

namespace App\Http\Middleware;

use App\Providers\RouteServiceProvider;
use Closure;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;
use Symfony\Component\HttpFoundation\Response;

class RedirectIfAuthenticated
{
    /**
     * Handle an incoming request.
     *
     * @param  \Closure(\Illuminate\Http\Request): (\Symfony\Component\HttpFoundation\Response)  $next
     */
    public function handle(Request $request, Closure $next, string ...$guards): Response
    {
        $guards = empty($guards) ? [null] : $guards;

        foreach ($guards as $guard) {
            if (Auth::guard($guard)->check()) {

                $multi_auth_guard = multi_auth_guard();

                if($guard === $multi_auth_guard) {

                    return to_route($multi_auth_guard .'.dashboard');

                }

                return redirect(RouteServiceProvider::HOME);
            }
        }

        return $next($request);
    }
}

※ 太字の部分を追加しています

ビューをつくる

ブラウザで表示されることになるHTMLを含んだコードをつくっていきます。

ログイン

resources/views/student/auth/login.blade.php

<x-guest-layout>
    <!-- Session Status -->
    <x-auth-session-status class="mb-4" :status="session('status')" />

    <form method="POST" action="{{ route($multi_auth_guard .'.login') }}">
        @csrf

        <!-- Email Address -->
        <div>
            <x-input-label for="email" :value="__('Email')" />
            <x-text-input id="email" class="block mt-1 w-full" type="email" name="email" :value="old('email')" required autofocus autocomplete="username" />
            <x-input-error :messages="$errors->get('email')" class="mt-2" />
        </div>

        <!-- Password -->
        <div class="mt-4">
            <x-input-label for="password" :value="__('Password')" />

            <x-text-input id="password" class="block mt-1 w-full"
                            type="password"
                            name="password"
                            required autocomplete="current-password" />

            <x-input-error :messages="$errors->get('password')" class="mt-2" />
        </div>

        <!-- Remember Me -->
        <div class="block mt-4">
            <label for="remember_me" class="inline-flex items-center">
                <input id="remember_me" type="checkbox" class="rounded border-gray-300 text-indigo-600 shadow-sm focus:ring-indigo-500" name="remember">
                <span class="ml-2 text-sm text-gray-600">{{ __('Remember me') }}</span>
            </label>
        </div>

        <div class="flex items-center justify-end mt-4">
            @if (Route::has('password.request'))
                <a class="underline text-sm text-gray-600 hover:text-gray-900 rounded-md focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500" href="{{ route($multi_auth_guard .'.password.request') }}">
                    {{ __('Forgot your password?') }}
                </a>
            @endif

            <x-primary-button class="ml-3">
                {{ __('Log in') }}
            </x-primary-button>
        </div>
    </form>
</x-guest-layout>

ユーザー登録

resources/views/student/auth/register.blade.php

<x-guest-layout>
    <form method="POST" action="{{ route($multi_auth_guard .'.register') }}">
        @csrf

        <!-- Name -->
        <div>
            <x-input-label for="name" :value="__('Name')" />
            <x-text-input id="name" class="block mt-1 w-full" type="text" name="name" :value="old('name')" required autofocus autocomplete="name" />
            <x-input-error :messages="$errors->get('name')" class="mt-2" />
        </div>

        <!-- Email Address -->
        <div class="mt-4">
            <x-input-label for="email" :value="__('Email')" />
            <x-text-input id="email" class="block mt-1 w-full" type="email" name="email" :value="old('email')" required autocomplete="username" />
            <x-input-error :messages="$errors->get('email')" class="mt-2" />
        </div>

        <!-- Password -->
        <div class="mt-4">
            <x-input-label for="password" :value="__('Password')" />

            <x-text-input id="password" class="block mt-1 w-full"
                            type="password"
                            name="password"
                            required autocomplete="new-password" />

            <x-input-error :messages="$errors->get('password')" class="mt-2" />
        </div>

        <!-- Confirm Password -->
        <div class="mt-4">
            <x-input-label for="password_confirmation" :value="__('Confirm Password')" />

            <x-text-input id="password_confirmation" class="block mt-1 w-full"
                            type="password"
                            name="password_confirmation" required autocomplete="new-password" />

            <x-input-error :messages="$errors->get('password_confirmation')" class="mt-2" />
        </div>

        <div class="flex items-center justify-end mt-4">
            <a class="underline text-sm text-gray-600 hover:text-gray-900 rounded-md focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500" href="{{ route($multi_auth_guard .'.login') }}">
                {{ __('Already registered?') }}
            </a>

            <x-primary-button class="ml-4">
                {{ __('Register') }}
            </x-primary-button>
        </div>
    </form>
</x-guest-layout>

パスワード再発行

resources/views/student/auth/forgot-password.blade.php

<x-guest-layout>
    <form method="POST" action="{{ route($multi_auth_guard .'.register') }}">
        @csrf

        <!-- Name -->
        <div>
            <x-input-label for="name" :value="__('Name')" />
            <x-text-input id="name" class="block mt-1 w-full" type="text" name="name" :value="old('name')" required autofocus autocomplete="name" />
            <x-input-error :messages="$errors->get('name')" class="mt-2" />
        </div>

        <!-- Email Address -->
        <div class="mt-4">
            <x-input-label for="email" :value="__('Email')" />
            <x-text-input id="email" class="block mt-1 w-full" type="email" name="email" :value="old('email')" required autocomplete="username" />
            <x-input-error :messages="$errors->get('email')" class="mt-2" />
        </div>

        <!-- Password -->
        <div class="mt-4">
            <x-input-label for="password" :value="__('Password')" />

            <x-text-input id="password" class="block mt-1 w-full"
                            type="password"
                            name="password"
                            required autocomplete="new-password" />

            <x-input-error :messages="$errors->get('password')" class="mt-2" />
        </div>

        <!-- Confirm Password -->
        <div class="mt-4">
            <x-input-label for="password_confirmation" :value="__('Confirm Password')" />

            <x-text-input id="password_confirmation" class="block mt-1 w-full"
                            type="password"
                            name="password_confirmation" required autocomplete="new-password" />

            <x-input-error :messages="$errors->get('password_confirmation')" class="mt-2" />
        </div>

        <div class="flex items-center justify-end mt-4">
            <a class="underline text-sm text-gray-600 hover:text-gray-900 rounded-md focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500" href="{{ route($multi_auth_guard .'.login') }}">
                {{ __('Already registered?') }}
            </a>

            <x-primary-button class="ml-4">
                {{ __('Register') }}
            </x-primary-button>
        </div>
    </form>
</x-guest-layout>

resources/views/student/auth/reset-password.blade.php

<x-guest-layout>
    <form method="POST" action="{{ route($multi_auth_guard .'.password.store') }}">
        @csrf

        <!-- Password Reset Token -->
        <input type="hidden" name="token" value="{{ $request->route('token') }}">

        <!-- Email Address -->
        <div>
            <x-input-label for="email" :value="__('Email')" />
            <x-text-input id="email" class="block mt-1 w-full" type="email" name="email" :value="old('email', $request->email)" required autofocus autocomplete="username" />
            <x-input-error :messages="$errors->get('email')" class="mt-2" />
        </div>

        <!-- Password -->
        <div class="mt-4">
            <x-input-label for="password" :value="__('Password')" />
            <x-text-input id="password" class="block mt-1 w-full" type="password" name="password" required autocomplete="new-password" />
            <x-input-error :messages="$errors->get('password')" class="mt-2" />
        </div>

        <!-- Confirm Password -->
        <div class="mt-4">
            <x-input-label for="password_confirmation" :value="__('Confirm Password')" />

            <x-text-input id="password_confirmation" class="block mt-1 w-full"
                                type="password"
                                name="password_confirmation" required autocomplete="new-password" />

            <x-input-error :messages="$errors->get('password_confirmation')" class="mt-2" />
        </div>

        <div class="flex items-center justify-end mt-4">
            <x-primary-button>
                {{ __('Reset Password') }}
            </x-primary-button>
        </div>
    </form>
</x-guest-layout>

Email verification

resources/views/student/auth/verify-email.blade.php

<x-guest-layout>
    <div class="mb-4 text-sm text-gray-600">
        {{ __('Thanks for signing up! Before getting started, could you verify your email address by clicking on the link we just emailed to you? If you didn\'t receive the email, we will gladly send you another.') }}
    </div>

    @if (session('status') == 'verification-link-sent')
        <div class="mb-4 font-medium text-sm text-green-600">
            {{ __('A new verification link has been sent to the email address you provided during registration.') }}
        </div>
    @endif

    <div class="mt-4 flex items-center justify-between">
        <form method="POST" action="{{ route($multi_auth_guard .'.verification.send') }}">
            @csrf

            <div>
                <x-primary-button>
                    {{ __('Resend Verification Email') }}
                </x-primary-button>
            </div>
        </form>

        <form method="POST" action="{{ route($multi_auth_guard .'.logout') }}">
            @csrf

            <button type="submit" class="underline text-sm text-gray-600 hover:text-gray-900 rounded-md focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500">
                {{ __('Log Out') }}
            </button>
        </form>
    </div>
</x-guest-layout>

Dashboard(ログイン後のページ)

resources/views/student/dashboard.blade.php

{{ $multi_auth_guard }} でログイン中です。

<form method="post" action="./logout">
    @csrf
    <button type="submit">ログアウト</button>
</form>

ルートをつくる

Laravel Breezeをインストールすると、何もしなくてもroutes/auth.phpというファイルが作成されますので、これをコピーし、routes/student.phpというファイルをつくり、中身を次のように変更します。

routes/student.php

<?php

use App\Http\Controllers\Student\AuthenticatedSessionController;
use App\Http\Controllers\Student\ConfirmablePasswordController;
use App\Http\Controllers\Student\EmailVerificationNotificationController;
use App\Http\Controllers\Student\EmailVerificationPromptController;
use App\Http\Controllers\Student\NewPasswordController;
use App\Http\Controllers\Student\PasswordController;
use App\Http\Controllers\Student\PasswordResetLinkController;
use App\Http\Controllers\Student\RegisteredUserController;
use App\Http\Controllers\Student\VerifyEmailController;
use Illuminate\Support\Facades\Route;

Route::prefix('student')->name('student.')->group(function () {

    Route::middleware('guest:student')->group(function () {
        Route::get('register', [RegisteredUserController::class, 'create'])
            ->name('register');

        Route::post('register', [RegisteredUserController::class, 'store']);

        Route::get('login', [AuthenticatedSessionController::class, 'create'])
            ->name('login');

        Route::post('login', [AuthenticatedSessionController::class, 'store']);

        Route::get('forgot-password', [PasswordResetLinkController::class, 'create'])
            ->name('password.request');

        Route::post('forgot-password', [PasswordResetLinkController::class, 'store'])
            ->name('password.email');

        Route::get('reset-password/{token}', [NewPasswordController::class, 'create'])
            ->name('password.reset');

        Route::post('reset-password', [NewPasswordController::class, 'store'])
            ->name('password.store');
    });

    Route::middleware('auth:student')->group(function () {
        Route::get('verify-email', EmailVerificationPromptController::class)
            ->name('verification.notice');

        Route::get('verify-email/{id}/{hash}', VerifyEmailController::class)
            ->middleware(['signed', 'throttle:6,1'])
            ->name('verification.verify');

        Route::post('email/verification-notification', [EmailVerificationNotificationController::class, 'store'])
            ->middleware('throttle:6,1')
            ->name('verification.send');

        Route::get('confirm-password', [ConfirmablePasswordController::class, 'show'])
            ->name('password.confirm');

        Route::post('confirm-password', [ConfirmablePasswordController::class, 'store']);

        Route::put('password', [PasswordController::class, 'update'])->name('password.update');

        Route::post('logout', [AuthenticatedSessionController::class, 'destroy'])
            ->name('logout');

        Route::middleware('verified_for_multi_auth')->group(function () {

            Route::get('dashboard', function () {
                return view('student.dashboard');
            })->name('dashboard');

        });
    });

});

routes/web.php

// 省略

require __DIR__.'/auth.php';
require __DIR__.'/student.php'; // 👈 ここを追加しました

なお、もし初期状態のuserを使ったログイン等が不要の場合は、

require __DIR__.'/auth.php';

の部分をコメントアウトや削除してもいいでしょう。

テストしてみる

今回の実行結果はLaravel Breezeと同じなのでテストは省略します(文章長くてちょっと疲れましたしね😂)

企業様へのご提案

Laravelの構成を根本から利用するすると、Multi Authだけでなくいろいろな改造を施すことができます。

もしそういった機能をご希望でしたらいつでもお気軽にお問い合わせからご相談ください。

お待ちしております。😄✨

おわりに

ということで今回はLaravel Breezeの機能をフルに活かしながらMulti Authを実装してみました。

とはいえ、今回は取り扱わなかったですが、以下のような機能もあるみたいですので、また機会があったらやってみたいと思います。

  • パスワードを入力しないとアクセスできないページをつくる
  • パスワードの変更

Laravel Breezeって機能多すぎですね(笑)

作者さんには感謝しています😄✨

また、終わってみてから思いついたのですが、Multi Authに対応したstubを作っておけば楽かもですね。

みなさんも、ぜひいろいろと改造してみてくださいね。

ではでは〜❗

「鬼滅の刃みたいに
真ん中で割れた石、
カッコよかったです👍」

開発のご依頼お待ちしております 😊✨
開発のご依頼はこちらから: お問い合わせ
どうぞよろしくお願いいたします!
このエントリーをはてなブックマークに追加       follow us in feedly  

開発効率を上げるための機材・まとめ