環境

PHP 8.1
Laravel 9.19

初めに

手早く実装場合はJetstreamとかBreezeを使用するといいと思います。

laravel / breeze
この記事では使用しませんがBreezeを参考に進めていきます。

モデル

Laravelの初期設定はUserモデルが認証で使用するモデルになるので、今回はこれをそのまま使用します。
初期状態ではMustVerifyEmailがコメントアウトしていると思うので、コメントを外して実装します。

Models/User.php

use Illuminate\Contracts\Auth\MustVerifyEmail;

class User extends Authenticatable implements MustVerifyEmail
{

確認メール用コントローラーの作成

新しいコントローラーを作成して、ざっくりと枠組みを作ってみましょう。
全部で3つのアクションメソッドを作ります。

index 確認メールの送信ボタンを表示する画面
notification メールを送信するアクション
verification 送信したメールアドレスに記載するリンクのアクション

Http/Controllers/Auth/EmailVerificationController.php

<?php

declare(strict_types=1);

namespace App\Http\Controllers\Auth;

use App\Http\Controllers\Controller;
use App\Models\User;
use App\Providers\RouteServiceProvider;
use Illuminate\Auth\Events\Verified;
use Illuminate\Http\RedirectResponse;
use Illuminate\Http\Request;
use Illuminate\View\View;


class EmailVerificationController extends Controller
{
	/**
	 * 確認メール送信画面
	 */
	public function index()
	{}

	/**
	 * 確認メール送信
	 */
	public function notification()
	{}

	/**
	 * メールリンクの検証
	 */
	public function verification()
	{}
}

ルートの設定

ルートの設定をします。
ミドルウェアにアクセス回数制限をするthrottleと期限付きURLを指定するsignedを設定してます。
URLは自由に変更できますが、nameはそのまま使うのがいいのではないかと思います。

routes/web.php

use App\Http\Controllers\Auth\EmailVerificationController;

Route::controller(EmailVerificationController::class)
	->prefix('email')->name('verification.')->group(function () {
		// 確認メール送信画面
		Route::get('verify', 'index')->name('notice');
		// 確認メール送信
		Route::post('verification-notification', 'notification')
			->middleware('throttle:6,1')->name('send');
		// 確認メールリンクの検証
		Route::get('verification/{id}/{hash}', 'verification')
			->middleware(['signed', 'throttle:6,1'])->name('verify');
	});

あとはメールの確認が必要なルートにverifiedを適用します。

Route::middleware(['web', 'verified', 'auth'])

これで設定したルートにアクセスした際メール確認をしていない場合は、「確認メール送信画面」にリダイレクトされます。

確認メール送信画面

メールの送信ボタンを配置する画面を作ります。
EmailVerificationControllerindexですね。

hasVerifiedEmailemail_verified_atカラムがnullでなかったらtrueを返します。
最終的にメール確認をするとこのemail_verified_atに日付が入るので、ここの処理ではメール確認している場合はホーム画面にリダイレクトして、未確認の場合はビューを表示するということをしています。

Http/Controllers/Auth/EmailVerificationController.php

/**
 * 確認メール送信画面
 *
 * @param  Request  $request
 * @return RedirectResponse|View
*/
public function index(Request $request): RedirectResponse|View
{
	return $request->user()->hasVerifiedEmail()
		? redirect()->intended(RouteServiceProvider::HOME)
		: view('auth.verify-email');
}

表示するビューを作成します。
セッションのverification-link-sentはメール送信後に設定します。
メール送信前に表示して、送信後にセッションを入れてまたこの画面に戻る流れです。

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

<div>
	<h1><a href="/">確認メールの送信</a></h1>
	<div>
		@if (session('status') === 'verification-link-sent')
			<p>
				登録したメールアドレスを確認してください!!
			</p>
			<p ><a href="/">TOPに戻る</a></p>
		@else
			<p>
				確認メールを送信してください!!
			</p>
			<form method="post" action="{{ route('verification.send') }}">
				@method('post')
				@csrf
				<div>
					<button type="submit">確認メールを送信</button>
				</div>
			</form>
		@endif
	</div>
</div>

これでルートに設定した、確認メールが必要な画面にアクセスするとこの画面に遷移します。

確認メール送信アクション

確認メール送信機能を作ります。
EmailVerificationControllernotificationですね。

ここではユーザーモデルの親クラスでトレイトしているMustVerifyEmailsendEmailVerificationNotificationを実行しています。
こいつを実行することでメールが送信されるようです。

Http/Controllers/Auth/EmailVerificationController.php

/**
 * 確認メール送信
 *
 * @param  Request  $request
 * @return RedirectResponse
 */
public function notification(Request $request): RedirectResponse
{
	/** @var User $user */
	$user = $request->user();

	// メール確認済みの場合はトップへ
	if ($user->hasVerifiedEmail()) {
		return redirect()->intended(RouteServiceProvider::HOME);
	}

	// メール送信
	$user->sendEmailVerificationNotification();

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

これで確認メールを送信できますが、今の状態だとメールのテンプレートがLaravelデフォルトのものなので使いにくいと思います。
カスタマイズするにはAuthServiceProvider.phpを次のbootに下記を追記します。

Providers/AuthServiceProvider.php

use Illuminate\Auth\Notifications\VerifyEmail;

// ...

public function boot()
{
	$this->registerPolicies();

	//

	VerifyEmail::toMailUsing(function ($notifiable, $url) {
		return (new MailMessage)
			->subject('メールアドレスの確認')
			->action('確認', $url)
			->view('emails.verify-email');
	});
}

viewで指定した場所にメールテンプレートを作りします。

resources/views/emails/verify-email.blade.php

リンクをクリックしてください!!<br>
<a href="{{ $displayableActionUrl }}">{{ $actionUrl }}</a>

メールのURLから飛んだ先のアクション

最後にメールのリンクから飛んできた先のアクションを作成します。
EmailVerificationControllerverificationですね。

ここもメール送信と同じく、MustVerifyEmailmarkEmailAsVerifiedを実行しています。
email_verified_atカラムに現在日時を入れてアップデートしているだけですね。
正常に完了した場合はホームへリダイレクトさせます。
URL(Hash)のチェックとかはルートで設定したsignedミドルウェアでやってくれます。

Http/Controllers/Auth/EmailVerificationController.php

/**
 * メールリンクの検証
 *
 * @param  Request  $request
 * @return RedirectResponse
 */
public function verification(Request $request): RedirectResponse
{
	/** @var User $user */
	$user = $request->user();

	if ($user->hasVerifiedEmail()) {
		return redirect()->intended(RouteServiceProvider::HOME.'?verified=1');
	}

	// email_verified_atカラムの更新
	if ($user->markEmailAsVerified()) {
		event(new Verified($user));
	}

	return redirect()->intended(RouteServiceProvider::HOME.'?verified=1');
}

email_verified_atに日付が入力されるとverifiedを設定したルートにアクセスできるようになります。