Email Verificationは以前のLaravelバージョンから存在する機能ですが、JetStreamでは設定ファイルを利用して有効化・無効化を行うことができます。

本文書ではJetstreamでのEmail Verification(メール検証)の設定方法と動作確認を行います。またどのような流れで処理が行われているかもコードを見ながら確認していきます。

Email Verificatationを完全に理解するためにはEvent&Listener, Notification, Mail, Signed URLなど複数のLaravel機能の理解も必要になります。

Email Verification(メールアドレス検証)とは

最近のサービスにはほとんど実装されてEmail Verificationはユーザ登録を行うと登録したメールアドレス宛にサービス側からメールが送信され、ユーザがそのメールを受信できることを確認してからサービスを利用を開始することができる機能です。

登録するユーザのEmailが実際に存在するものなのか、登録したメールに入力間違いがなくユーザに正しく届いているかを確認するためのものです。最近は知らない番号からの電話はとらない人が増えているのでユーザが入力したメールに間違いがある場合に対応のコストがかかるのでこの機能があれば時間の無駄も省くことができます。

大まかな流れを下記の通りです。

  1. ユーザがユーザ登録を行う
  2. ユーザ登録時に入力したメールアドレスに対してアプリケーション(Laravel)からメール検証メールを送信する
  3. ユーザがアプリケーション(Laravel)から届いたメールを開き、メール内のメール検証ボタンをクリックする。
  4. ボタンにはリンクが貼られておりクリックするとサイトにリダイレクトされ、検証がアプリケーション(Laravel)のサーバ上で行われる。リンクには検証に必要な情報が含まれている。
  5. 検証が完了するとユーザのテーブルの列に検証完了の情報が登録される
  6. アプリケーション(Laravel)にアクセスすると検証完了のチェックが行われ、検証が完了していればアクセス可能(ユーザのログインも必要)となる
  7. アクセスの度に検証が完了しているのかチェックが行われる

Laravel環境の構築

JetstreamをLaravelのインストールと同時に行うため–jetオプションをつけます。


 % laravel new laravel_email_validation --jet

livewireとinertiaどちらをインストールするか聞かれるのでここではlivewireを選択します。inertiaを選択しても構いません。


Which Jetstream stack do you prefer?
  [0] livewire
  [1] inertia
 > 0

teamsを利用するかどうかも確認されるのでデフォルトのnoを選択します。


 Will your application use teams? (yes/no) [no]:

インストールが開始されるのでインストール後はプロジェクトディレクトリに移動してnpm install && npm run devを実行してください。


% cd laravel_email_validation/
 % npm install && npm run dev

データベースは簡易的に利用できるsqliteを利用するためdatabase.sqliteファイルを作成します。


 % touch database/database.sqlite

作成後.envファイルを開いてDB_CONNECTIONをsqliteに設定します。それ以外のDB_*の設定項目は削除してください。更新後の.envファイルは下記のようになります。DBに関する設定値のみ表示しています。


DB_CONNECTION=sqlite

php artisan migrateコマンドを実行してテーブルの作成を行ってください。


 % php artisan migrate
Migration table created successfully.
Migrating: 2014_10_12_000000_create_users_table
Migrated:  2014_10_12_000000_create_users_table (3.22ms)
Migrating: 2014_10_12_100000_create_password_resets_table
Migrated:  2014_10_12_100000_create_password_resets_table (1.49ms)
Migrating: 2014_10_12_200000_add_two_factor_columns_to_users_table
Migrated:  2014_10_12_200000_add_two_factor_columns_to_users_table (1.35ms)
Migrating: 2019_08_19_000000_create_failed_jobs_table
Migrated:  2019_08_19_000000_create_failed_jobs_table (1.13ms)
Migrating: 2019_12_14_000001_create_personal_access_tokens_table
Migrated:  2019_12_14_000001_create_personal_access_tokens_table (2.39ms)
Migrating: 2020_10_21_010548_create_sessions_table
Migrated:  2020_10_21_010548_create_sessions_table (1.79ms)

これでEmail Verificationを実行するLaravel環境は整いました。

メール送信環境構築

Email Verificationの機能を有効するとユーザ登録後にユーザが入力したメールアドレスに向けてLaravelからメールが送信されます。

動作確認では何度も入力したメールアドレスにメールを送信することになるのでダミーのメールサーバを準備しメールの設定を行います。本文書ではダミーのメールサーバとしてmailtrap.ioのサービスを利用します。無料で利用できるSMTPサーバでLaravelから送信されるメールはすべてmailtrapに保存されます。

ここではmailtrapを利用していますが、本番環境のメールサーバを利用しても問題はありません。

Laravelのメールの設定は.envファイルに記述されています。Laravelでもデフォルトはmailtrapを利用することを想定しており下記のようにMAIL_HOSTはsmtp.mailtrap.ioが設定されています。


MAIL_MAILER=smtp
MAIL_HOST=smtp.mailtrap.io
MAIL_PORT=2525
MAIL_USERNAME=null
MAIL_PASSWORD=null
MAIL_ENCRYPTION=null
MAIL_FROM_ADDRESS=null
MAIL_FROM_NAME="${APP_NAME}"

MailTrapの設定は下記を参考にしてください。

MailTrapで取得した接続情報を.envファイルに設定します。XXXXは環境によって異なります。


MAIL_MAILER=smtp
MAIL_HOST=smtp.mailtrap.io
MAIL_PORT=2525
MAIL_USERNAME=XXXX
MAIL_PASSWORD=XXXX
MAIL_ENCRYPTION=tls
MAIL_FROM_ADDRESS=test@example.com
MAIL_FROM_NAME="${APP_NAME}"
MAIL_FROM_ADDRESSとMAIL_FROM_NAMEはmailtrapの情報には含まれず各自が設定する項目です。MAIL_FROM_ADDRESSには送信元のメールアドレスを設定してください。MAIL_FROM_NAMEは.envファイルのAPP_NAMEが設定されます。一部の環境変数をいれなかった場合はconfig/mail.phpの設定ファイルで指定されている値が設定されます。MAIL_FROM_ADDRESSが未設定の場合はhello@example.comになります。

機能の有効化

Email Verificationの機能はデフォルトでは有効になっていないので以下の2つのファイルを確認する必要があります。

  • Fortify.php
  • User.php

Fortifyの設定ファイルであるconfig¥fortify.phpファイルのFeature::emailVerification()のコメントが外れているか確認します。デフォルトではemailVerificationにはコメントがついています。

FortifyはJetStreamの中で認証に関する処理を行うパッケージです。Email VerificationもFortifyの機能の一つのためFortify.phpの設定ファイルで機能の有効・無効を設定します。

下記のようになっていればFortifyのすべての機能は有効になっています。ここではregistrationとemailVerificationに注目してください。他の機能は無効になっていてもEmail Verificationには影響はありません。registrationが無効だとユーザ登録画面が利用できません。


'features' => [
    Features::registration(),
    Features::resetPasswords(),
    Features::emailVerification(),
    Features::updateProfileInformation(),
    Features::updatePasswords(),
    Features::twoFactorAuthentication(),
],

次にapp¥models¥User.phpファイルを確認します。Use.phpファイルではMustVerifyEmailのインターフェイスをimplementsする必要があります。

MustVerifyEmail自体はデフォルトでuseされているので下記の設定のみを行ってください。


class User extends Authenticatable implements MustVerifyEmail

以上でEmail Verificationの設定は完了です。

ユーザの登録

設定が完了したら、/registerにアクセスしユーザの登録画面からユーザ登録を行います。

ユーザ登録画面
ユーザ登録画面

登録するユーザの情報を入力したら、REGISTERボタンをクリックしてください。

このままダッシュボードにリダイレクトした場合は設定がうまく行われていないので設定を確認してください。Email Verificationの設定が正常に動作している場合は下記の画面が表示されます。この画面が表示されたら入力したEmailアドレスに対してメールが送信されているはずです。本環境ではmailtrap.ioにアクセスしInboxを確認します。

検証確認画面
検証確認画面

もし下記のエラーが発生した場合は、.envファイルで送信元のメールアドレスが設定されていないことが原因なので設定を行ってください。他にもメール設定の不備がある場合はエラーが表示されるはずです。

メール送信失敗
メール送信失敗

送信されるメールの内容

本文書ではMailtrapを利用しているのでMailtrapのInboxにメールを検証するためのメールが届いています。通常はユーザ登録を行ったユーザのメールアドレス宛に送信されます。

toには入力したjohn@exmple.comが設定されています。FromにはLaravel(.envのAPP_NAME)と.envで設定したMAIL_FROM_ADDRESSで設定したtest@example.comが設定されていることがわかります。

MailtrapのInbox
MailtrapのInbox

中身を見ると英語です。メールアドレスを検証するために送信されているメールなので問題がなければVerify Email Address(Emailアドレスを検証する)をクリックすることになります。英語の日本語化については本文書の後半に説明を行っています。

メールの本文
メールの本文

クリックするとメール検証がリンク先であるLaravel上で行われ完了するとユーザはそのままアプリケーションにアクセスすることができます。デフォルトでは/dashboardの画面が表示されます。

メールの検証が完了しないまま、/dashboard等のURLを知っているユーザがいたとしてもアクセスすることはできません。そのためにはmiddlewareのverifiedを設定しておく必要があります。そのためデフォルトのweb.phpのdashboardへのルーティングにmiddlewareのverifiedが設定されています。

これでEmail Verificationの動作確認は完了です。Email Verificationを利用したいだけであればここまでの理解で十分です。

次はEmail VerificationではLaravelでどのような処理が行われているのか確認を行っていきます

処理の流れの確認

いつどのタイミングでメールを送信する処理が実行されているのか確認を行いさらに英語メールを日本語メールにするための方法を確認していきます。

最初に実行される処理

ユーザの登録画面で入力した内容は/registerに対してPOSTリクエストが送信されます。POSTリエクストが送信された後に実行される処理はphp artrisan routes:listコマンドで確認することができます。

URI列でregisterを探しMethod列でPOSTを探してください。そのAction列が実行される処理で下記のファイルとメソッドです。

Laravel\Fortify\Http\Controllers\RegisteredUserController@store

RegisteredUserController.phpファイルのstoreメソッドを見るとDependency Injectionでインスタンス化された$creatorでユーザの作成を行っていることがわかります。

JetStreamでの認証に関する文書は下記が参考になります。ユーザ作成の流れも説明しています。

ユーザが作成された後はイベントのRegisteredクラスの引数に作成された$userが指定されeventが発行されています。メール検証の処理の中でユーザが作成されるのではなくユーザ作成後にメール検証が行われます。


public function store(Request $request,
                        CreatesNewUsers $creator): RegisterResponse
{
    event(new Registered($user = $creator->create($request->all())));

    $this->guard->login($user);

    return app(RegisterResponse::class);
}

イベントとリスナーの確認

イベントのクラスであるRegisteredの中身を確認します。Registerd.phpではconstructorで引数の$userを$user変数に設定しています。リスナー側でこの$userを利用して処理を行います。。


class Registered
{
    use SerializesModels;

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

イベントの内容がわかったのでそのイベントを受け取るリスナーの確認を行います。

どのイベントにどのリスナーが対応しているかはapp¥Providers¥EventServiceProvider.phpファイルに記述されています。

中身を確認するとRegisteredイベントに対応するリスナーはSendEmailVerificationNotificationクラスであることがわかります。名前からもEmail Verificationに関係するものだということがわかります。


protected $listen = [
    Registered::class => [
        SendEmailVerificationNotification::class,
    ],
];

リスナーの処理

リスナーではhandleメソッドが実行されます。handleメソッドでは下記の条件がチェックされています。

  • $event->userがMustVerifyEmailのインスタンスであること
  • $event->userのhasVerifiedEmailメソッドがfalseであること

2つの条件が揃った場合にsendEmailVerificationNotificationメソッドが実行されることがわかります。


public function handle(Registered $event)
{
    if ($event->user instanceof MustVerifyEmail && ! $event->user->hasVerifiedEmail()) {
        $event->user->sendEmailVerificationNotification();
    }
}

各条件の処理をもう少し細かく見ていきます。

$eventはイベントクラスであるRegistredのインスタンスなので$userを保持しています(先ほどRegistered.phpファイルで確認)。

$userはapp¥Models¥User.phpのインスタンスでメール検証機能を利用するためにMustVeriyEmailをimplementsしました。そのためメール検証の設定を正しく行っていれば$event->userはMustVerifyEmailのインスタンスとなります。

hasVerifiedEmailメソッドは、MustVerifyEmail.phpに登録されおり、$userが持つemail_verified_atの値がnullかどうかチェックを行っています。email_verified_atはuserテーブルの列の一つでデフォルトではnullが入ります。

email_verified_atがuserテーブルにあるかどうかはマイグレーションファイルで確認できます。email_verified_at列がnullかどうかはユーザが登録した後メールに表示されているボタンを押す前にuserテーブルにアクセスして値を確認してください。

2つの条件式でtrueとなる場合にsendEmailVerificationNotificationメソッドが実行されることになります。

ここからの処理は$user(User.php)が主役の処理でUser.phpクラスに設定されているtraitを利用して処理を行っています。

EmailVerificationの設定でMustVerifyEmailをimplementsしていない場合はFortifyでemailVerificationを有効にしていてもメール検証のメールが送信されることはありません。それはsendEmailVerificationNotificationメソッドが実行されないからです。

notifyメソッドの確認

sendEmailVerificationNotificationメソッドもMustVErifyEmail.phpファイルにメソッドとして登録されており、その中でnotifyメソッドが実行されています。

nofityが実行されるこの場所がメールが送信処理を行う場所です。

notifyメソッドはUserクラスが持つtraitのNoticiableさらにRoutesNotificationにあります。


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

notifyの引数のVerifyEmailはLaravelのNotification機能を利用する際に作成するNotificationクラスです。notifyメソッドについてはLaravelの通常の機能で引数にNotificationクラスを指定することで通知処理を行うことができます。notifyの処理についてはEmail Verification用に用意されている特別の機能ではありません。

Notificationについては本ブログでもNotificationに関する詳細な情報は載せていないのでLaravelのドキュメントで確認を行ってください。

VerifyEmailクラスの確認

どのような内容のメールが送信されるかはすべてVerifyEmailの中に記述されています。VerifyEmailはNotificationクラスでメール通知を行うのでviaメソッドとtoMailメソッドを持ちどちらも$notifiableの変数を引数に持ちます。$notifiableの実体は$userです。

viaメソッドではどうやって通知を行うか設定を行います。Email Verificationはメールの通知なのでmailが設定されています。


public function via($notifiable)
{
    return ['mail'];
}

toMailメソッドではメールの内容とメールの中のボタンに設定されているリンクのURLを作成する処理が入っています。


public function toMail($notifiable)
{
    $verificationUrl = $this->verificationUrl($notifiable);

    if (static::$toMailCallback) {
        return call_user_func(static::$toMailCallback, $notifiable, $verificationUrl);
    }

    return (new MailMessage)
        ->subject(Lang::get('Verify Email Address'))
        ->line(Lang::get('Please click the button below to verify your email address.'))
        ->action(Lang::get('Verify Email Address'), $verificationUrl)
        ->line(Lang::get('If you did not create an account, no further action is required.'));
}

URLの作成

送信されている検証メールに設定されているURLの作成はverificationUrlメソッドで行っています。


$verificationUrl = $this->verificationUrl($notifiable);

verificationUrlメソッドを確認するとstatic::$createUrlCallbackとありますがこれはもしURLを作成する方法をLaravelが準備したものとは異なる独自のものを利用したい場合に設定を行えるように準備されています。独自の方法でURLを作らないのであれば無視してください。

その後URL::temporarySignedRouteメソッドでSigned URLを作成しています。Signed URLは文字列にハッシュが加えられたURLを生成し変更が加えられていないかどうか検証を行うことができるURLです。URLに有効期限を設定することができます。検証を行うえるURLを作成するということはそれを検証する機能が必要となります。それは後ほどの処理で説明を行います。

検証メールに付与されたSigned URLをユーザがクリックして検証にパスすればアクセスが可能となります。


protected function verificationUrl($notifiable)
{

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

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

URL作成後次の処理に移りますが、static::$toMailCallbackのチェックが行われています。これは先ほどの$createUrlCallbackと同じ働きを持っており、Laravelで送信するNotificationメールとは異なるメールの内容を送信したい時に$toMailCallbackを設定することで別の処理を行うことが可能です。デフォルトの形式で問題ない場合またLaravelが準備したデフォルトのテンプレートを編集する場合は無視してもかまいません。

$createUrlCallback, $toMailCallbackを利用した動作確認は行った経験がないので間違いがあるかもしれません。

最後にMailMessageクラスを利用して送信するメールの内容を設定しています。


if (static::$toMailCallback) {
    return call_user_func(static::$toMailCallback, $notifiable, $verificationUrl);
}

return (new MailMessage)
    ->subject(Lang::get('Verify Email Address'))
    ->line(Lang::get('Please click the button below to verify your email address.'))
    ->action(Lang::get('Verify Email Address'), $verificationUrl)
    ->line(Lang::get('If you did not create an account, no further action is required.'));

メールの日本語設定

動作確認した時に受信したメールはすべて英語で記述されていました。日本のサービスであればすべての英語で送信するわけにはいきません。ここでは日本語化の方法を説明していきます。

MailMessageのsubject、lineメソッドではLangファサードが利用されているので各テキストに対応する日本語を表示させることができます。

config¥app.phpファイルのlocaleをjaに設定し、resource¥langの下にja.jsonファイルを作成します。

英語に対応する日本語をJSONファイルに記述することでメールの内容を日本語に変換することができます。ja.jsonファイルを利用して試しに以下を設定します。


{
    "Verify Email Address":"メールアドレスの検証する",
    "Please click the button below to verify your email address.":"メールアドレスの検証を行うため下記のボタンをクリックしてください。",
    "If you did not create an account, no further action is required.":"もしアカウントを作成していない場合は追加の処理は必要ありません。"
}

日本語の対応を記述した項目についてはすべて英語から日本語に変換されているのは確認できます。しかしLang::getの対応だけですべての英語が日本語になるわけではありません。Hello!, Regards,その下のテキストは英語のままです。

一部日本語化
一部日本語化

残りの英語を日本語化するためにはsubjectやlineなどのメソッドではなくメールに利用されているテンプレート自体を確認する必要があります。

php artisan vendor:publishを下記のように実行するとnotificationのテンプレートファイルがresources/views/vendor/notificationsの下に作成されます。ファイル名はemail.blade.phpです。


 % php artisan vendor:publish --tag=laravel-notifications
Copied Directory [/vendor/laravel/framework/src/Illuminate/Notifications/resources/views] To [/resources/views/vendor/notifications]
Publishing complete.

中身を確認すると送信したメールの内容に記述されているHello!の単語を見つけることができます。また冒頭の挨拶であればgreetingメソッドが使えそうだということもこのファイルから確認です。


@component('mail::message')
{{-- Greeting --}}
@if (! empty($greeting))
# {{ $greeting }}
@else
@if ($level === 'error')
# @lang('Whoops!')
@else
# @lang('Hello!')
@endif
@endif
//略
MailMessageのメソッドを確認したい場合はSimpleMessage.phpファイルを確認してください。geetingメソッドもあります。またRegardsの変わりに$salutationを設定することもできます。

先ほどのja.jsonファイルを利用してまHello!がこんにちはに変更できるか確認してみましょう。


{
    "Verify Email Address":"メールアドレスの検証する",
    "Please click the button below to verify your email address.":"メールアドレスの検証を行うため下記のボタンをクリックしてください。",
    "If you did not create an account, no further action is required.":"もしアカウントを作成していない場合は追加の処理は必要ありません。",
    "Hello!":"こんにちは"
}

ユーザの登録を行うとHello!がこんにちはに変更されていることが確認できます。

Hello!をこんにちはへ
Hello!をこんにちはへ

あとは同じようにRegards,とその下の英語のメッセージを日本語に変えましょう。

Regardsについては日本語化することはできました。しかし変数を含む下記は日本語化させることはできませんでした。文字列全体であれば日本語化できました。変数についての処理はBladeの理解をさらに進めれば対応できるかもしれません。


@lang(
    "If you’re having trouble clicking the \":actionText\" button, copy and paste the URL below into your web browser:",
    [
        'actionText' => $actionText,
    ]
) 

日本語化の対応ができなかった場合はemail.blade.phpを直接更新することで対応することも可能です。


@lang(
    "もし \":actionText\" ボタンがクリックできない場合は、下部に表示されているURLを直接ブラウザにコピー&ペーストしてください。",
    [
        'actionText' => $actionText,
    ]
) 

再度実行すると日本語化できることが確認できます。

日本語化の対応
日本語化の対応

メールのテンプレートの確認

メールの内容は日本語化することはできました。しかし、Laravelのロゴが残っている状態です。このままメールを送信するわけにはいきませんがemail.blade.phpファイルにはロゴの設定場所はありませんでした。

ロゴだけではなくこのメールを見たときにメールのフォーマット自体も変えたいと思った人もいるかもしれません。その場合はメールのテンプレートファイルを更新する必要があります。

下記のphp artisan vendor:publishコマンドを実行するとresouces¥views¥vendor¥mailディレクトリにファイルが作成されます。


 % php artisan vendor:publish --tag=laravel-mail        
Copied Directory [/vendor/laravel/framework/src/Illuminate/Mail/resources/views] To [/resources/views/vendor/mail]
Publishing complete.

mail¥htmlの下にあるmessage.blade.phpファイルでlayout, header, footrのコンポーネントが利用されていることがわかります。


@component('mail::layout')
{{-- Header --}}
@slot('header')
@component('mail::header', ['url' => config('app.url')])
{{ config('app.name') }}
@endcomponent
@endslot

{{-- Body --}}
{{ $slot }}

{{-- Subcopy --}}
@isset($subcopy)
@slot('subcopy')
@component('mail::subcopy')
{{ $subcopy }}
@endcomponent
@endslot
@endisset

{{-- Footer --}}
@slot('footer')
@component('mail::footer')
© {{ date('Y') }} {{ config('app.name') }}. @lang('All rights reserved.')
@endcomponent
@endslot
@endcomponent

layoutファイルを確認するとstyleタグやcssで設定が行われていることがわかります。styleを変更したい場合はlayoutファイルを更新することで対応できそうです。

ロゴが設定されているheader.blade.phpファイルを確認してみましょう。

$slotに入っている値によってLaravelのロゴが表示されるように設定されています。この$slotにはmessage.blade.phpから{{ config(‘app.name’) }}が入ることがわかります。


<tr>
<td class="header">
<a href="{{ $url }}" style="display: inline-block;">
@if (trim($slot) === 'Laravel')
<img src="https://laravel.com/img/notification-logo.png" class="logo" alt="Laravel Logo">
@else
{{ $slot }}
@endif
</a>
</td>
</tr>

ここまでは.envファイルではAPP_NAMEをデフォルトのLaravelを設定していたのでこの値をReffectに変更します。


APP_NAME=Reffect

再度ユーザ登録を行いメールを確認すると下記のようにロゴは消えてAPP_NAMEに設定した値が反映されます。

ロゴから文字列へ
ロゴから文字列へ

自サービスのロゴを設定したい場合はheader.blade.phpを更新することで対応可能だということがわかります。

メール内容の更新方法を理解することができました。

クリックを押した後の処理

メール検証ボタンを押すとどこにリダイレクトされるかはメールの下部のリンクで確認することができます。

ボタンのリンク先
ボタンのリンク先

リンクを見ると/email/verify/{id}/{hash}になっていることがわかります。php artisan route:listコマンドで対応する処理を確認してみましょう。

確認すると下記であることがわかります。

Laravel\Fortify\Http\Controllers\VerifyEmailController@__invoke 

しかし、上記の内容を確認する前に認証のパッケージのFortifyがルーティング登録を行っているvendor¥laravel¥fortify¥routes¥routes.phpを確認します。

FortifyServiceProvider.phpのconfigureRoutesメソッドで認証に関連するルーティングの登録が行われています。

Email Verificationについては3つのルーティングの登録がおこなわれていますが、今回はアクセスするURLが/email/verify/{id}/{hash}の形なので2番目のルーティングを確認します。


// Email Verification...
if (Features::enabled(Features::emailVerification())) {
    Route::get('/email/verify', [EmailVerificationPromptController::class, '__invoke'])
        ->middleware(['auth'])
        ->name('verification.notice');

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

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

複数のmiddlewareが設定されていますが、ここで注目するのはsignedです。このmiddlewareがURLの検証を行います。

middlewareのsignedがどのような処理を行っているかを確認するため、middlewareの設定が記述されているapp¥Http¥Kernel.phpファイルを確認します。

signedのmiddlewareの処理を行うのはValidateSignatureファイルであることがわかります。


protected $routeMiddleware = [
//略
    'signed' => \Illuminate\Routing\Middleware\ValidateSignature::class, //ここ
    'throttle' => \Illuminate\Routing\Middleware\ThrottleRequests::class,
    'verified' => \Illuminate\Auth\Middleware\EnsureEmailIsVerified::class,
];

ValidateSignature.phpファイルではhasValidSignatureメソッドが実行されています。


public function handle($request, Closure $next, $relative = null)
{
    if ($request->hasValidSignature($relative !== 'relative')) {
        return $next($request);
    }

    throw new InvalidSignatureException;
}

hasValidSignatureメソッドの処理についてはLaravelのマニュアルに記載されており、正しい署名かどうか検証が行われます。

マニュアルには”To verify that an incoming request has a valid signature, you should call the hasValidSignature method on the incoming Request”と記載されています。

署名に問題がないことが確認でき他のmiddlewareの処理でも問題がなければ、最終的にVerifyEmailController@__invokeに進みます。

VerifyEmailController@__invokeの中身を見てみましょう。


public function __invoke(VerifyEmailRequest $request): RedirectResponse
{
    if ($request->user()->hasVerifiedEmail()) {
        return redirect()->intended(config('fortify.home').'?verified=1');
    }

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

    return redirect()->intended(config('fortify.home').'?verified=1');
}

hasVerifiedEmailメソッドは本文書でも2回目の登場ですが、MustVerifyEmail.phpに登録されており、ユーザのemail_verified_at列がnullかどうかのチェックを行っています。もうメール検証が完了していればそのまま/dashboard(fortify.home)にリダイレクトされます。

次にmarkEmailAsVerifiedメソッドが実行されemail_verified_at列に時刻の設定を行っています。これでemail_verified_at列がnullから時刻に変わり、今後はhasVerifiedEmailメソッドでtrueを戻すようになります。


public function markEmailAsVerified()
{
    return $this->forceFill([
        'email_verified_at' => $this->freshTimestamp(),
    ])->save();
}

ここまでの処理が終えるとユーザはアプリケーションにアクセスすることができます。

markEmailAsVerifiedの処理が完了した後にはeventが設定されています。Verifiedに対応するリスナーの登録を確認することができませんでしたが検証後になにか別の処理を行いたい場合はリスナーを登録して処理を追加することが可能です。


event(new Verified($request->user()));

メールによる検証が完了した後もmiddlewareにverifiedを設定することにより、毎回アクセス時にユーザのemail_verified_atに値があるかチェックを行っています。


Route::middleware(['auth:sanctum', 'verified'])->get('/dashboard', function () {
    return view('dashboard');
})->name('dashboard');

verifiedのmiddlewareもsingedと同様にkernel.phpが登録されており、対応するクラスはEnsureEmailsVerifiedです。

中身を確認するとhasVerifiedEmailメソッドと$request->userがMustVerifyEmail()のインスタンスかチェックを行っています。これは本文書の”リスナーの処理”で確認したチェック方法と同じものです。

Email Verificationの機能を有効でemail_verified_atに値がない場合はエラーになるかログイン画面にリダイレクトされます。


public function handle($request, Closure $next, $redirectToRoute = null)
{
    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 ?: 'verification.notice'));
    }

    return $next($request);
}

この文書ではSigned URLの説明が不足していますがその処理のコードをより詳細に確認し理解できればEmail Verificationの機能の実装ができるレベルになれるかと思います。