Laravel パスワード再設定メールをカスタムしてみた

##はじめに
この記事はプログラミング初学者による備忘録用の記事であり、また、少しでも他の初学者のお役に立てればと思い書いています。

今回はlaravelでパスワード再設定メールをカスタムしてみたので、まとめてみました。
間違いなどがございましたら、ご指摘のほどよろしくお願い致します。

##この記事の流れ
まずはじめに、どのようにLaravelのパスワード再設定メールの送信が行われているのかを各クラスの説明も含めてざっくりと解説します。その後に実際にカスタムしていきたいと思います。

結論だけ知りたい方は、実装の箇所まで飛んでください。

##Laravelのパスワード再設定メールの送信がどのように行われるのか
パスワード再設定メールの送信は、Userモデルがインポートしているトレイトのメソッドである、``sendPasswordResetNotificationメソッド``で行われています。
従って、独自の通知(パスワード再設定メール)を作りたい場合、``Userクラス(モデル)``に``sendPasswordResetNotificationメソッド``を定義して、**オーバーライド**することで可能になります。

ちなみに、``sendPasswordResetNotificationメソッド``は、Userクラスが継承している親Userクラス(Illuminate\Foundation\Auth\User)が利用している``CanResetPasswordトレイト``の中に定義されています。


親User.phpは下記のようになっています。

```Illuminate\Foundation\Auth\User.php
<?php

namespace Illuminate\Foundation\Auth;

use Illuminate\Auth\Authenticatable;
use Illuminate\Auth\MustVerifyEmail;
use Illuminate\Auth\Passwords\CanResetPassword;
//略
class User extends Model implements
    AuthenticatableContract,
    AuthorizableContract,
    CanResetPasswordContract
{
    use CanResetPassword, //略
}
```

CanResetPassword.phpは下記のようになっています。

```Illuminate\Auth\Passwords\CanResetPassword.php
<?php

namespace Illuminate\Auth\Passwords;

use Illuminate\Auth\Notifications\ResetPassword as ResetPasswordNotification;

trait CanResetPassword
{
    /**
     * Get the e-mail address where password reset links are sent.
     *
     * @return string
     */
    public function getEmailForPasswordReset()
    {
        return $this->email;
    }

    /**
     * Send the password reset notification.
     *
     * @param  string  $token
     * @return void
     */
     //ここにsendPasswordResetNotificationメソッドがあります
    public function sendPasswordResetNotification($token)
    {
        $this->notify(new ResetPasswordNotification($token));
    }
}
```


話が少し逸れましたが、要するに、
独自のパスワード再設定メールを送信したい場合、**``UserモデルでsendPasswordResetNotificationメソッドをオーバーライドする必要があります。``**

オーバーライドする為には、下記手順を踏む必要があります。


1. ``Mailableクラス``を継承しているクラスを作成する。``Mailableクラス``を使うことでテキストメールの送信が可能になるので、継承しているクラスを作成する。

2. ``Mailableクラス``を継承しているクラスを用いたメール送信メソッドを作成する為に、``Notificationクラス``を作成する。

3. ``Notificationクラス``で``mailableクラス``のメソッドを用いた(Mailableインスタンスを返す必要がある)メール送信メソッドを作成し、メールの詳細を設定する

4. UserモデルでsendPasswordResetNotificationメソッドを使い、作成した``Notificationクラス``をインスタンス化することで、オーバライドする。


何故、オーバーライドすることで、パスワード再設定メールをカスタムできるかについては、パスワードリセット周りの実装のコードを読むと分かります。
下記リンク先にて先人が詳細を述べていますので、参考にしつつコードを辿ってみてください。

https://qiita.com/t-kuni/items/aa906cd07e54037a5eaf#%E5%87%A6%E7%90%86%E6%A6%82%E8%A6%81

上記の手順説明で登場した、``mailableクラス``、``Notificationクラス``について詳しく見ていきたいと思います。

##Mailableクラスとは
Laravelでは、Mailableクラスを使うことで、アプリケーションが``テキストメールを送信する``ことを可能にします。

ちなみに、もう一つlaravelにはメールを取り扱うクラスが存在しており、MailMessageクラスと言います。このクラスは``HTMLメール``を送ることが可能です。今回は汎用性が高いテキストメールを作りたいと思います。

Mailableクラスは、``vendor/laravel/framework/src/Illuminate/Mailディレクトリ``に存在しています。基本的にMailableクラスは継承して扱いますが、アプリケーションに最初からMailableクラスを継承しているクラスは存在していません。

従って、``make:mail Artisanコマンド``を使用して作成する必要があります。コマンド入力後、app/Mailディレクトリが作成され、下記のようなMailableクラスを継承しているクラスが作成されます。


```app/Mail/TestMail.php
<?php

namespace App\Mail;

use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Mail\Mailable;
use Illuminate\Queue\SerializesModels;

class TestMail extends Mailable
{
    use Queueable, SerializesModels;

    /**
     * Create a new message instance.
     *
     * @return void
     */
    public function __construct()
    {
        //
    }

    /**
     * Build the message.
     *
     * @return $this
     */
    public function build()
    {
         return $this->view('view.name');
    }
}
```

##Mailableクラスを継承したクラス内で様々なメソッドを呼び出すことができる
**Mailableクラス**を継承しているクラスでメールの詳細設定を行う場合、``buildメソッド``の中で行います。このメソッドの中で、Mailableクラスのfrom、subject、view、textなどさまざまなメソッドを呼び出し、メールの詳細を設定します。

単純なメール送信などを行う場合は、Notificationクラスを使わずに、Mailableクラスを継承しているクラスのbuildメソッド内でメールの詳細を設定することがベストプラクティスかなと思っています。(設定が複雑でない為実装しやすい)

Mailableクラスの各メソッドの詳細はリンク先をお読み下さい。

``参考:``[laravel 6.x メール 日本語ドキュメント](https://readouble.com/laravel/6.x/ja/mail.html
)

##Notificationクラスとは
ドキュメントの説明が一番分かりやすいので引用しておきます。

>
Laravelの各通知は、（通常、app/Notificationsディレクトリに設置される）クラスにより表されます。このディレクトリがアプリケーションで見つからなくても、心配ありません。make:notification Artisanコマンドを実行すると、作成されます。
このコマンドにより、真新しい通知クラスが、app/Notificationsディレクトリに生成されます。各通知クラスはviaメソッドと、特定のチャンネルに最適化したメッセージへ変換する、いくつかのメッセージ構築メソッド（toMail、toDatabaseなど）を含んでいます。

>

``引用元:``[laravel 6.x 通知](https://readouble.com/laravel/6.x/ja/notifications.html)

##Notificationクラスを用いて通知を送信することができる
Laravelには、通知を送信する方法が主に2つあり、``Notifiableトレイトのnotifyメソッド``か``Notificationファサード``を使う方法です。
両者共に作成したNotificationクラスをインスタンス化して扱う点は同じです。

今回のパスワード再設定メール送信のカスタムでは、既存のパスワード再設定メール送信の処理で``notifyメソッド``が使われているので、Notifiableトレイトのnotifyメソッドを選択することでオーバーライドできるようにします。


**では、実際に先ほど述べた手順でカスタムしていきたいと思います。**

##1.Mailableクラスを継承したクラスを作成する
まずは、mailableクラスを継承しているクラスを作成する為に、下記コマンドを実行します。

```
$ php artisan make:mail TestMail(クラス名)
```

実行することで、下記のような/app/Mail/TestMail.phpが作成されます。

```/app/Mail/TestMail.php
<?php

namespace App\Mail;

use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Mail\Mailable;
use Illuminate\Queue\SerializesModels;

class TestMail extends Mailable
{
    use Queueable, SerializesModels;

    /**
     * Create a new message instance.
     *
     * @return void
     */
    public function __construct()
    {
        //
    }

    /**
     * Build the message.
     *
     * @return $this
     */
    public function build()
    {
	return $this->view('view.name');  //これを削除
    return $this; //追記します
    }
}
```

**ここで重要な点は、**
``Mailableクラス``を継承したクラス(TestMailクラス)を作成した理由です。

後に登場するNotificationクラスのメソッド(``toMailメソッド``)で、Mailableクラスのメソッドを使いテキストメールの詳細設定を行うには、Notificationクラスで**Mailableクラスのインスタンスを返す必要があります。**
このような理由から、今回のMailableクラスを継承しているクラスは、NotificationクラスでMailableクラスのメソッドを使う為に作成したので、メールの詳細はbuildメソッドで設定しません。

従って、``buildメソッドでは空設定のメールを返すように変更します。``

##2.Notificationクラスを作成後、Mailableクラスのメソッドを使いNotificationクラスのメソッド内でメールの詳細を設定する

まずはNotificationクラスを作成する為に、下記コマンドを実行します。

```
$ php artisan make:notification PasswordResetNotification.php
```

実行することで、下記のような``/app/Notifications/PasswordResetNotification.php``
が作成されます。(変更点をコメント表記で書いてあります)

```/app/Notifications/PasswordResetNotification.php
<?php

namespace App\Notifications;

//追加
use App\Mail\TestMail;
//追加
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Notifications\Messages\MailMessage;
use Illuminate\Notifications\Notification;

class PasswordResetNotification extends Notification
{
    use Queueable;

    //追加
    public $token;
    public $mail;
    //追加
    /**
     * Create a new notification instance.
     *
     * @return void
     */
    public function __construct(string $token, TestMail $mail) //この行を変更
    {
        //追加
        $this->token = $token;
        $this->mail = $mail;
        //追加
    }

    /**
     * Get the notification's delivery channels.
     *
     * @param  mixed  $notifiable
     * @return array
     */
    public function via($notifiable)
    {
        return ['mail'];
    }

    /**
     * Get the mail representation of the notification.
     *
     * @param  mixed  $notifiable
     * @return \Illuminate\Notifications\Messages\MailMessage
     */
    public function toMail($notifiable)
    {
	    //削除
        return (new MailMessage)
             ->line('The introduction to the notification.')
             ->action('Notification Action', url('/'))
             ->line('Thank you for using our application!');
        //削除        
	    //追加
        return $this->mail
            ->from(config('mail.from.address'), config('mail.from.name'))
            ->to($notifiable->email)
            ->subject('[memo]パスワード再設定')
            ->text('emails.password_reset')
            ->with([
                'url' => route('password.reset', [
                    'token' => $this->token,
                    'email' => $notifiable->email,
                  ]),
                'count' => config(
                    'auth.passwords.' .
                    config('auth.defaults.passwords') .
                    '.expire'
                ),
            ]);
        //追加
    }
    //略
}
```

**ここで重要な点は、**
route(password.email)で渡ってくる$tokenと、Mailableクラスを継承したクラスをコンストラクタの引数で受け取り、作成したプロパティに代入する点です。

まず、route(password.email)で渡ってくる``$token``をコンストラクタで受け取る理由は、後のtoMailメソッド(メールの詳細を決めるメソッド)のroute(password.reset)で、route(password.email)から渡ってきた``$token``を使うことになるので、予め渡ってくる``$token``を``Notificationクラスのプロパティ($token)``に代入する為にコンストラクタで受け取り、プロパティ に代入します。

Mailableクラスを継承したクラスをコンストラクタで受け取る理由は、``toMailメソッドでMailableクラスをインスタンス化する必要がある``為、予めコンストラクタの引数で``classの型宣言をすることでMailableクラスを継承したクラスのインスタンス``をコンストラクタにてDIするようにしています。

___

###追記したコードの解説

```/app/Notifications/PasswordResetNotification.php

    public $token;
    public $mail;

    /**
     * Create a new notification instance.
     *
     * @return void
     */
    public function __construct(string $token, TestMail $mail)
    {
        $this->token = $token;
        $this->mail = $mail;
    }
```

先ほど述べたように、プロパティを新規作成、コンストラクタでroute(password.email)で渡ってくる``$token``と``TestMailクラスのインスタンス``を引数として設定し、新規作成したプロパティに代入しています。

___
```/app/Notifications/PasswordResetNotification.php
 public function via($notifiable)
    {
        return ['mail'];
    }
```
viaメソッドで通知方法を選択するのですが、 今回は、メールという1つの通知方法しかないので決め打ちします。

___

```/app/Notifications/PasswordResetNotification.php
public function toMail($notifiable)
    {
     return $this->mail
        ->from(config('mail.from.address'), config('mail.from.name'))
        ->to($notifiable->email)
        ->subject('[syokumane]パスワード再設定')
        ->text('emails.password_reset')
        ->with([
                'url' => route('password.reset', [
                    'token' => $this->token,
                    'email' => $notifiable->email,
                ]),
                'count' => config(
                    'auth.passwords.' .
                     config('auth.defaults.passwords') .
                    '.expire'
                ),
              ]);
    }
```
toMailメソッドで、メールの詳細を設定します。簡単に見ていきたいと思います。

```
 return $this->mail
```

先ほど述べたように、NotificationクラスのtoMailメソッドでは、**Mailableクラスのインスタンスを返す必要がある**ので、コンストラクタで作成した$this->mailを持ってきます。

```
from(config('mail.from.address'), config('mail.from.name'))
```
Mailableクラスが提供している``fromメソッド``といいます。
config関数を使うことで、``config/mail.php``以下の値を取得しています。

アプリケーションで同じfromアドレスを全メールで使用する場合、グローバルなfromアドレスをconfig/mail.php設定ファイルで設定するべきだと思います。
下記のように、.envファイルの環境変数を使って設定します。

```config/mail.php
'from' => [
        'address' => env('MAIL_FROM_ADDRESS', 'hello@example.com'),
        'name' => env('MAIL_FROM_NAME', 'Example'),
    ],
```


```
to($notifiable->email)
```
``toメソッド``には、送信先メールアドレスを渡します。
``$notifiable``には、パスワード再設定メール送信先となるUserクラス(モデル)のインスタンスが代入されています。
従って、``$notifiable->email``で、パスワード再設定メールの送信先であるユーザーのメールアドレスを取得しています。

何故、``$notifiable``にUserクラスのインスタンスが入っているのか説明すると
**Userクラス(モデル)で``Notifiableトレイト``をuseしているからです。**

つまり、``送信先にしたいクラスでNotifiableトレイトをuseするだけで、$notifiableを送信先として指定したクラスのインスタンスとして使えるようになります。``すごい。

```
subject(‘[password_reset]パスワード再設定')
```
``subjectメソッド``には、メールの件名を渡します。

```
text('emails.password_reset')
```
``textメソッド``は、**テキスト形式のメールを送る場合に使うメソッドです。**
引数で、メールのテンプレートディレクトリ先を指定します。

text('emails.password_reset')とすることで、``resources/views/emailsディレクトリのpassword_reset.blade.php``がテンプレートとして使用されます。


```
->with([
    'url' => route('password.reset', [
        'token' => $this->token,
        'email' => $notifiable->email
    ]),
    'count' => config(
        'auth.passwords.' .
        config('auth.defaults.passwords') .
        '.expire'
    ),
]);
```
テンプレートとなるBladeに渡す変数を、``withメソッド``に連想配列形式で渡します。

```
'url' => route('password.reset', [
    'token' => $this->token,
    'email' => $notifiable->email
]),
```
keyである``url``の値には、``route関数を使ってpassword.resetのルーティングをセットします。``
emailはpassword.resetのルーティングに記述されていませんが、``クエリストリング``としてURLに追加されます。

URLは下記のようになります。


```
http://localhost/password/reset/(トークン)?email=(メールアドレス)
```
クエリストリングに関しては、下記サイトをご覧ください。

[クエリ文字列 【query string】 クエリストリング](https://e-words.jp/w/%E3%82%AF%E3%82%A8%E3%83%AA%E6%96%87%E5%AD%97%E5%88%97.html)

```
'count' => config(
    'auth.passwords.' .
    config('auth.defaults.passwords') .
    '.expire'
),
```
keyである``count``の値には、パスワード設定画面へのURLの有効期限(分単位)がセットされます。

config/auth.php以下を見てみると、デフォルトで60分となっています。

```config/auth.php
'passwords' => [
        'users' => [
            'provider' => 'users',
            'table' => 'password_resets',
            'expire' => 60,
            'throttle' => 60,
        ],
    ],
```

**テキストメールのテンプレート書き方**
``例``

```resources/views/emails/password_reset.blade.php
下記のURLからパスワードの再設定を行って下さい。

{{ $url }}

URLの有効期間は{{ $count }}分です。
```

上記のように、withメソッドで作成した変数を使い、URLを作成、有効期間をユーザーに知らせるように設定します。

##3.UserクラスでsendPasswordResetNotificationメソッドをUserモデルに定義してオーバーライドする

```app/Models/User.php
 public function sendPasswordResetNotification($token)
{
$this->notify(new PasswordResetNotification($token, new BareMail()));
}
```

先ほど作成した通知クラスのインスタンスを生成し、notifyメソッドに渡します。
これでカスタムしたテキストメールがパスワード再設定メールとして送信されるようになります。

##おわりに
パスワード再設定メールの処理を読み解くだけでもかなりのコード量でした。

既存のコードを読むことはとても参考になるので、今後も継続していきたいと思います。

次はLaravel側でのメール送信を「キュー」と呼ばれる仕組みを使った「非同期処理」が行えるらしいので調べてみたいと思います。

最後までお読み頂きありがとうございました。

##参考文献
[laravel 6.x メール 日本語ドキュメント](https://readouble.com/laravel/6.x/ja/mail.html
)

[laravel 6.x 通知 日本語ドキュメント](https://readouble.com/laravel/6.x/ja/notifications.html)

[laravel 6.x パスワードリセット 日本語ドキュメント](https://readouble.com/laravel/6.x/ja/passwords.html)

[Laravelの実装調査　～パスワードリセット編～](https://qiita.com/t-kuni/items/aa906cd07e54037a5eaf#%E5%87%A6%E7%90%86%E6%A6%82%E8%A6%81)