Fortify利用におけるパスワードリセットのviewとメールのカスタマイズ


基本的な機能だけ用意されたFortify
実利用となると結構カスタマイズしなきゃいけない気がしている

今回はパスワードリセットのviewとメールまわりを修正したくなった時に
いじった場所を上げていきます
基本的にメソッド上書きして対応です

# カスタマイズ対象
- viewの変更
- 任意のメールの送信
    - Notificationsを使う

パスワードリセットのプロセスを変更したい場合は
App\Actions\ResetUserPassword を変更するといいよと公式では言ってます
https://readouble.com/laravel/9.x/ja/fortify.html#resetting-the-password

しかし、メールとviewの差し替えだけなんだよなぁと思い
今回は対象箇所だけミニマムに対応していこうかと思います

## viewのカスタマイズ

~~~php:app/Providers/FortifyServiceProvider.php
        
        // パスワード忘れた人への画面
        // メールアドレスを入力させる
        Fortify::requestPasswordResetLinkView(function () {
            return view('auth.forget-password'); // 直すのここ
            // view先でPOSTを{{ route('password.email') }}にしておく
        });

        // 新規パスワード入力画面
        Fortify::resetPasswordView(function ($request) {
            return view('auth.reset-password', ['request' => $request]); // 直すのここ
            // view先でPOSTを{{ route('password.update') }}にしておく
        });
~~~



## 送信メールのカスタマイズ
送るメールクラスを変更したいのでカスタマイズしていきます

### sendPasswordResetNotificationメソッドを追記

~~~php:app/Models/User.php
use App\Notifications\PasswordResetNotification;

class User extends Authenticatable
    /**
     * パスワードリセット通知の送信を上書き
     *
     * @param  string  $token
     * @return void
     */
    public function sendPasswordResetNotification($token): void
    {
        // パスワードリセットリクエストがあった時に行いたい処理をここに書く
        // 今回はNotificationを差し替え
        $this->notify(new PasswordResetNotification($token)); 
    }
~~~


### PasswordResetNotificationを作る
resetUrl()でリセットリンクを発行してMailクラスに渡してあげる

~~~php:app/Notifications/PasswordResetNotification.php
<?php

namespace App\Notifications;

use Illuminate\Bus\Queueable;
use Illuminate\Notifications\Notification;

use App\Mail\PasswordResetMail; // ご希望のメールクラスを

class PasswordResetNotification extends Notification
{
    use Queueable;

    public $token;
    protected $subject = '【madree】パスワードリセットのお知らせ';

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

    /**
     * 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)
    {
        $to = $notifiable->email;
        $url = $this->resetUrl($notifiable);

        $options = [
            'from' => config('mail.from.address'),
            'from_jp' => config('mail.from.name'),
            'to' => $to,
            'subject' => $this->subject,
            'template' => 'mail.passwordreset',
        ];
        $data = [
            'url' => $url,
        ];
        // ご希望のメールクラスを指定
        $mail = new PasswordResetMail($options, $data);

        return $mail->to($to);
    }

    /**
     * Get the array representation of the notification.
     *
     * @param  mixed  $notifiable
     * @return array
     */
    public function toArray($notifiable)
    {
        return [
            //
        ];
    }

    /**
     * Get the reset URL for the given notifiable.
     * src/Illuminate/Auth/Notifications/ResetPassword.php から拝借
     *
     * @param  mixed  $notifiable
     * @return string
     */
    protected function resetUrl(mixed $notifiable): string
    {
        return url(route('password.reset', [
            'token' => $this->token,
            'email' => $notifiable->getEmailForPasswordReset(),
        ], false));
    }
}

~~~


## 参考: Fortify元の仕組みを見ておく

### メール送信呼び出し元
メール忘れた人への画面からのPOST先
Fortifyの中にあるPasswordResetLinkController内のstoreにてsendResetLink()で呼ばれてます

https://github.com/laravel/fortify/blob/1.x/src/Http/Controllers/PasswordResetLinkController.php#L41
~~~php:vendor/laravel/fortify/src/Http/Controllers/PasswordResetLinkController.php
class PasswordResetLinkController extends Controller
{
    // ......
    public function store(Request $request): Responsable
    {
        // ....
        $status = $this->broker()->sendResetLink(
            $request->only(Fortify::email())
        );
~~~

そのあとはAuthのPasswordBrokerのなかのPasswordBroker()が呼ばれる
callbackは今回ないのでuserに生えたsendPasswordResetNotification()が次に実行される
https://github.com/laravel/framework/blob/9.x/src/Illuminate/Auth/Passwords/PasswordBroker.php#L48

~~~php:src/Illuminate/Auth/Passwords/PasswordBroker.php#L48
class PasswordBroker implements PasswordBrokerContract
{
    // ...
    public function sendResetLink(array $credentials, Closure $callback = null)
    {
        // ...
        if ($callback) {
            $callback($user, $token);
        } else {
            // Once we have the reset token, we are ready to send the message out to this
            // user with a link to reset their password. We will then redirect back to
            // the current URI having nothing set in the session to indicate errors.
            $user->sendPasswordResetNotification($token);
        }
~~~

sendPasswordResetNotificationはUserで読み込まれている
use Illuminate\Foundation\Auth\User as Authenticatable;
に定義されている

以下のように掘り進めると
vendor/laravel/framework/src/Illuminate/Foundation/Auth/User.php
↓
vendor/laravel/framework/src/Illuminate/Auth/Passwords/CanResetPassword.php

https://github.com/laravel/framework/blob/9.x/src/Illuminate/Auth/Passwords/CanResetPassword.php#L25

~~~php:src/Illuminate/Auth/Passwords/CanResetPassword.php#L25
use Illuminate\Auth\Notifications\ResetPassword as ResetPasswordNotification;

trait CanResetPassword
{
    public function sendPasswordResetNotification($token)
    {
        $this->notify(new ResetPasswordNotification($token));
    }
~~~


ResetPasswordNotificationは
よくあるNotificationで
メール(vendor/laravel/framework/src/Illuminate/Notifications/Messages/MailMessage.php)を送っている


# 最後に
パスワードリセットにおけるviewとメール周りのカスタマイズを書いてきました。
Userモデル内のsendPasswordResetNotification()で
わざわざNotification使わずに、力強くMailクラスでsendしてもよかったのかなと思ったりもしましたが、
上書きしているコードの流儀に沿った方がいいかなという気持ちもあり、
そのままNotificationを新規で作るというやりかたにしました。
Fortifyは自由度高くて困っちゃうことが多々あるんですが、適宜カスタムをやっていくしかないかなと思ってます。

---
弊社では、一緒に働いてくれるエンジニアを募集しています。


↓興味のある方はこちらから↓
[Wantedly - スタジオアンビルト株式会社](https://www.wantedly.com/companies/studiounbuilt)

こんなWebサービスを作っています。
・[マドリー](https://madree.jp/)
・[Studio Unbuilt](https://www.studiounbuilt.com/)