Laravel5.7のEmail Verificationを読む - Laravelコードリーディング

# 本項のテーマ & 前置き

Laravel5.7のコードを読んでいきます。
5.6 →　5.7でいくつかの機能が追加されました。そのうちの一つがEmail Verificationです。
早い話がユーザー登録時のメール確認ですね。以下の点をコード上で確認してみたいと思います。

+ ユーザー登録→確認メール送信
+ メール記載のURLをクリック→登録完了
+ メール確認済みかどうかのチェック

この機能の使い方（コーディング方法など）についは以下の記事がわかりやすいかと思いますのでそちらを参考にしてください（無許可）

[最新Laravel 5.7の便利な機能「正しいメールアドレスのみ登録させる機能：Email Verification」](https://qiita.com/daisu_yamazaki/items/d5962ec2143adbbbc2bd)

# Let's コードリーディング!!

## 確認メールを送るまで

まずは登録時の処理がどう変わっているのか見ていきましょう。
登録処理はIlluminate\Foundation\AuthRegistersUsersトレイトで実装されています。

``` vendor/laravel/framework/src/Illuminate/Foundation/Auth/RegistersUsers.php

public function register(Request $request)
{
    $this->validator($request->all())->validate();

    event(new Registered($user = $this->create($request->all())));

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

    return $this->registered($request, $user)
                    ?: redirect($this->redirectPath());
}
```

特に変わった様子はありません。ここで確認メールを送ってるかと思ったのですが違うみたいです。
Registeredイベントを使ってメール送信をしているのかもしれません。イベントサービスプロパイダーを覗いてみましょう。

``` app/Providers/EventServiceProvider.php

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

```

RegisteredイベントにSendEmailVerificationNotificationハンドラーを登録していました。では、SendEmailVerificationNotificationクラスを見てみます。

``` vendor/laravel/framework/src/Illuminate/Auth/Listeners/SendEmailVerificationNotification.php

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

` $event->user `のuserはAuthRegistersUsersトレイトでRegisteredイベントのコンストラクタの引数として渡していたUserクラスのインスタンスです。つまり、UserクラスのsendEmailVerificationNotificationメソッドを呼び出しています。Userクラスを読み進めて見ましょう。

App\Userクラスは親クラスはIlluminate\Foundation\Auth\Userクラスです。こちらを見てみます。

``` vendor/laravel/framework/src/Illuminate/Foundation/Auth/User.php

class User extends Model implements
    AuthenticatableContract,
    AuthorizableContract,
    CanResetPasswordContract
{
    use Authenticatable, Authorizable, CanResetPassword, MustVerifyEmail;
}

```

MustVerifyEmailトレイトをuseしてますね。SendEmailVerificationNotificationクラスでもUserクラスのインスタンスがMustVerifyEmailインターフェースかどうかチェックしていました。MustVerifyEmailトレイトの方を見て見ましょう。

※ 実はここで気になる点があります。[後ほど](https://qiita.com/yamaji_daisuke/items/731868a4de6037794976#気になる点20190915-内容修正)解説したいと思います。

``` vendor/laravel/framework/src/Illuminate/Auth/MustVerifyEmail.php

trait MustVerifyEmail
{

    public function hasVerifiedEmail()
    {
        return ! is_null($this->email_verified_at);
    }

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

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

```

sendEmailVerificationNotificationメソッドでnotifyしてます。そのままNotifications\VerifyEmailクラスを確認して見ましょう。

``` vendor/laravel/framework/src/Illuminate/Auth/Notifications/VerifyEmail.php

public function toMail($notifiable)
{
    if (static::$toMailCallback) {
        return call_user_func(static::$toMailCallback, $notifiable);
    }

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

protected function verificationUrl($notifiable)
{
    return URL::temporarySignedRoute(
        'verification.verify', Carbon::now()->addMinutes(60), ['id' => $notifiable->getKey()]
    );
}

```

おめでとう！あなたは目的地に到達しました！このtoMailメソッドで確認メールを送っています。そしてverificationUrlメソッドで確認用のURLを作成しています。URL::temporarySignedRouteを使用していますので、60分で無効になる署名URLになっているようです。（Laravelの署名URLについては[このへん](https://readouble.com/laravel/5.7/ja/urls.html#signed-urls)を参考にしてください。）

## メール記載のURLクリックから確認まで

メールに記載されているURLは` verification.verify `というルート名でした。どうせVerificationControllerクラスのverifyメソッドなので見てみましょう（適当）

``` app/Http/Controllers/Auth/VerificationController.php

use Illuminate\Foundation\Auth\VerifiesEmails;

class VerificationController extends Controller
{
    use VerifiesEmails;

    public function __construct()
    {
        $this->middleware('auth');
        $this->middleware('signed')->only('verify');
        $this->middleware('throttle:6,1')->only('verify', 'resend');
    }
}

```

例によってtraitで実装してるみたいです。Illuminate\Foundation\Auth\VerifiesEmailsトレイトに進みましょう。

``` vendor/laravel/framework/src/Illuminate/Foundation/Auth/VerifiesEmails.php

public function verify(Request $request)
{
    if ($request->route('id') == $request->user()->getKey() &&
        $request->user()->markEmailAsVerified()) {
        event(new Verified($request->user()));
    }

    return redirect($this->redirectPath());
}

```

UserクラスのmarkEmailAsVerifiedメソッドでいかにも何かしてそうです。このmarkEmailAsVerifiedメソッドはUserクラスが継承するIlluminate\Foundation\Auth\UserクラスでuseしているMustVerifyEmailトレイトに実装されていましたね。すでに一度引用していますが、以下再掲します。

``` vendor/laravel/framework/src/Illuminate/Auth/MustVerifyEmail.php

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

```

email_verified_atカラムを更新しています。このemail_verified_atカラムは
(公式のリリースノート)[https://laravel.com/docs/5.7/releases#laravel-5.7] にも書いてあるように、今回の更新でu追加されました。一応マイグレーションファイルを確認してみましょう。

``` database/migrations/2014_10_12_000000_create_users_table.php

Schema::create('users', function (Blueprint $table) {
    $table->increments('id');
    $table->string('name');
    $table->string('email')->unique();
    $table->timestamp('email_verified_at')->nullable(); /// ← こいつが追加
    $table->string('password');
    $table->rememberToken();
    $table->timestamps();
});

```

さぁ、これでメール確認が完了しました。ですが、メール確認したかどうかのチェックをしている箇所も確認しておいたほうがいいでしょう。

## メール確認が済んでいるかどうかのチェックはどこ？

ユーザーの認証チェックはミドルウェアでやってました。なのでメール確認済みかどうかもミドルウェアでやってるに違いありません（根拠なし）。なのでKernelクラスを確認して該当するミドルウェアがないかみて見ましょう。


``` app/Http/Kernel.php

protected $routeMiddleware = [
    'auth' => \App\Http\Middleware\Authenticate::class,
    'auth.basic' => \Illuminate\Auth\Middleware\AuthenticateWithBasicAuth::class,
    'bindings' => \Illuminate\Routing\Middleware\SubstituteBindings::class,
    'cache.headers' => \Illuminate\Http\Middleware\SetCacheHeaders::class,
    'can' => \Illuminate\Auth\Middleware\Authorize::class,
    'guest' => \App\Http\Middleware\RedirectIfAuthenticated::class,
    'signed' => \Illuminate\Routing\Middleware\ValidateSignature::class,
    'throttle' => \Illuminate\Routing\Middleware\ThrottleRequests::class,
    'verified' => \Illuminate\Auth\Middleware\EnsureEmailIsVerified::class, /// ← きっとこいつ
];

```

あった（一勝）。Illuminate\Auth\Middleware\EnsureEmailIsVerifiedクラスに進みます。

``` vendor/laravel/framework/src/Illuminate/Auth/Middleware/EnsureEmailIsVerified.php

class EnsureEmailIsVerified
{
    public function handle($request, Closure $next)
    {
        if (! $request->user() ||
            ($request->user() instanceof MustVerifyEmail &&
            ! $request->user()->hasVerifiedEmail())) {
            return $request->expectsJson()
                    ? abort(403, 'Your email address is not verified.')
                    : Redirect::route('verification.notice');
        }

        return $next($request);
    }
}

```

UserクラスのhasVerifiedEmailメソッドでチェックしてそうです。このhasVerifiedEmailメソッドはUserクラスが継承するIlluminate\Foundation\Auth\UserクラスでuseしているMustVerifyEmailトレイトに実装されていましたね。（コピペ）

``` vendor/laravel/framework/src/Illuminate/Auth/MustVerifyEmail.php
trait MustVerifyEmail
{
    public function hasVerifiedEmail()
    {
        return ! is_null($this->email_verified_at);
    }

```

email_verified_atをチェックしています。これで5.7の標準認証もバッチリだな？

## 気になる点（2019.09.15 内容修正）

さて、注釈で言っていた気になる点についで話をしてみます。

Illuminate\Foundation\Auth\Userクラスの基本的な実装方針としては、「interfaceをimplementしておいて、traitで実装を書く」形になってます。このクラスは3つのインターフェイスをimplementしていますが、各々のtraitとの対応関係は以下のようになります。

| interface       |    trait    |
|:-----------------|:------------------|
| Illuminate\Contracts\Auth\Authenticatable(AuthenticatableContract) | Illuminate\Auth\Authenticatable |
| Illuminate\Contracts\Auth\Access\Authorizable(AuthorizableContract) | Illuminate\Foundation\Auth\Access\Authorizable |
| Illuminate\Contracts\Auth\CanResetPassword(CanResetPasswordContract) | Illuminate\Auth\Passwords\CanResetPassword |

本来であれば、Illuminate/Foundation/Auth/UserクラスはIlluminate\Contracts\Auth\MustVerifyEmailインターフェイスをimplementした上でIlluminate\Auth\MustVerifyEmailトレイトをuseするのが正しい流れなのですが、なぜかUserクラスはMustVerifyEmailインターフェイスをimplementしていません。

なぜでしょう？
これはEmail Verificationの機能を使いたい場合のみ使えるようにするためのようです。確認メールを送信していたSendEmailVerificationNotificationクラスをもう一度見てみましょう。

``` vendor/laravel/framework/src/Illuminate/Auth/Listeners/SendEmailVerificationNotification.php

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

` instanceof MustVerifyEmail `でチェックしています。つまり、MustVerifyEmailインターフェイスをimplementsしていない場合はメールが送信されないようになっています。このおかげで、App\UserクラスでMustVerifyEmailインターフェイスをimplementしない限りメール確認機能が使えない形になっています。

## 念のためrouteをちゃんと確認する？

routeファイルで認証のための` Auth::routes(); `というコードが書かれていますが、引数にちょっと足します。

``` routes/web.php
Auth::routes(['verify' => true]);
```

こうするとEmail Verification周りのrouteが設定されます。どんなrouteか見てみましょう。

|ルート名|アクション|ミドルウェア|
|:--|:--|:--|
| verification.resend | App\Http\Controllers\Auth\VerificationController@resend | web,auth,throttle:6,1  |
| verification.notice | App\Http\Controllers\Auth\VerificationController@show | web,auth |
| verification.verify | App\Http\Controllers\Auth\VerificationController@verify | web,auth,signed,throttle:6,1 |

予想通りだな（小並感）

# 終わりに

間違いあったら指摘おなしゃーす。