ラムネグから一言:寝る前に読むとくだらなすぎて逆に寝れると好評なすごい適当なブログをこっちではじめてます.
laravelでユーザー認証機能を作ったときってやっぱりメールアドレス変更機能も必要ですよね。
なぜかlaravelにはパスワード再設定機能は公式で用意してあるのに、メールアドレスに関しては公式ドキュメントのどこにも言及がない…。なんでなんでしょう。海外だといったん登録したメールアドレスを変更するユーザーがすごく少ないんでしょうか…。
言っていてもしょうがないんでlaravelでメールアドレス変更機能を一番簡単に作る方法を紹介しますね。
新たにnotifyを作る事もしないですし、あらかじめあるlaravelのメール認証機能に沿った実装となります。ちなみにlaravelはバージョン10でやってますが、バージョン関係なくOKだと思います。
変更するのは4ファイルのみ
超簡単!という事で今回編集するファイルは4ファイルのみです。
- new_emailテーブル作成
- メルアド変更受付時にメール送信するように変更
- Userモデル変更
- verification.verifyの変更
それでは一つずつ見ていきましょう。
①「new_email」モデル&テーブル作成
まずは新しいメールアドレスを一時保存するテーブルを作成します。
Userにnew_emailみたいなカラムを持たせてもいいんですが、どうせなら誰がどういったメールアドレス変更を行ったのか履歴が残った方がいいのかなーと思ったのでUserとは別に、new_mailというテーブルを作りました。
new_mailのミグレーションファイルの中身
$table->id();
$table->unsignedBigInteger('user_id');
$table->string('email');
$table->timestamps();
マイページのメルアド変更画面(というか登録情報編集画面?)とかでメルアドの変更を受けたら、Userテーブルのemailを直接更新するんじゃなくて、new_mailテーブルに「このユーザーさんがこのメールアドレスに変更したいんだって!」っていうのを追加していきます。
なのでUserテーブルのメールアドレスはこのタイミングでは変更前の古いアドレスのまま。
②認証メール送信
さっき、マイページとかダッシュボードの登録情報編集画面よりメルアドの変更を受け付けたコントローラー内でnew_mailに「このユーザーがこのメルアドに変更したいんだって!」というのを新規追加したのち、$req->user()->sendEmailVerificationNotification();
を呼び出してlaravelに認証メール送信を行わせます。これは認証メール再送信として公式ドキュメントに載ってる方法になります。
③User.phpの編集
ただこのままだと$req->user()->sendEmailVerificationNotification();
はUserテーブルのemailに入ってるメルアド当て、つまり古いメルアドに対して認証メールを送っちゃうので宛先を変更します(また同時に認証メールに含まれる署名付きURLについてもこれで新メールの方に変更できています。くわしくは後述)。
で、やるのがメールアドレス再設定機能を導入したいモデルの編集です。
今回は、というかたぶん多くの場合でそうなるであろうUserモデルが対象として書いていきますね。
User.php
public function new_mails()
{
return $this->hasMany(NewMail::class);
}
public function getEmailForVerification()
{
if( ($this->hasVerifiedEmail() )
&& $this->new_mails()->exists() ){
$new_mail = $this->new_mails()->orderby('created_at', 'desc')->first();
$email = $new_mail->email;
}else{
$email = $this->email;
}
return $email;
}
関数が二つありますが、一つ目は普通のリレーションの設定です。
NewMailモデルとはhasManyの関係(一人のユーザーが何度もメルアド変更することもあると想定)なのでそういうふうに指定。ここでは書けてないですし、今回やらなくても動きますがNewMailモデルの方にもUserモデルとのリレーションを書いといてくださいね。
2つ目の関数名のgetEmailForVerification()といのがキモで、これオーバーライドしています(詳しくは後述)。なので絶対この関数名にしてくださいね。
ちなみにオーバーライドするには別に「オーバーライドするよ!」っていう何か特殊な書き方があるわけじゃなくて、そのまま継承元のクラスと同じ関数(今回の場合はgetEmailForVerification)をUser.phpに普段通り書けば勝手にオーバーライドされます。
ここではすでにemail_verified_atに値が入っているかどうか、nullじゃないかどうかで判断していて、もう値が入ってるなら「メールアドレスの再設定だな!ならUserテーブルのメルアドじゃなくてNewMailの方のメルアドで認証メールださないとな!」と判断しています(なのでelseの中がオーバーライドする前のgetEmailForVerificationになります)。これによって、Userテーブルのアドレスが古いままでも、new_mailテーブルに格納されている新しいメルアドに向けて認証メールが発行できるようになります。
きちんとやるのならUserテーブルに「new_mail_flag」みたいなカラムを一つ新規で作って、そのカラムがtrueならメルアド変更だ!と判断するようにしてもいいかも。
(※hasVerifiedEmail()関数はあらかじめ「MustVerifyEmail.php」で宣言されているので誰でも利用できます。やってることはemail_verified_atの値がnullかどうか判定してるだけの超シンプル関数です。後述)
「誰でも」というよりモデル内でMustVerifyEmailを継承してるハズ(User.php)
class User extends Authenticatable implements MustVerifyEmail
{
・
・
・
④コントローラーの編集
最後にコントローラーの編集です。
laravelの公式ドキュメントにある「verification.verify」を編集します。これは実際にユーザーがメール内の認証リンクを踏んだときの処理を行っている関数です。
変更前:
Route::get('/email/verify/{id}/{hash}', function (EmailVerificationRequest $request) {
$request->fulfill();
return redirect('/home');
})->middleware(['auth', 'signed'])->name('verification.verify');
変更後:
Route::get('/email/verify/{id}/{hash}', function (EmailVerificationRequest $request) {
$user = Auth::user();
if( $user->hasVerifiedEmail() ){
/* メールアドレス再設定時の認証 */
$new_mail = $user->new_mails()->orderby('created_at', 'desc')->first();
if( User::where('email', $new_mail->email)->exists() ){
return redirect()->route('○○');
}else{
$user->email = $new_mail->email;
$user->save();
return redirect()->route('△△');
}
}else{
/* 新規の時の認証 */
$request->fulfill();
return redirect('/home');
}
})->middleware(['auth', 'signed'])->name('verification.verify');
というふうにここでもhasVerifiedEmail()関数を使って「お、email_verified_atにすでに値が入ってるじゃん!ならメール再設定の方だな!」というのを判定して、後は一応Userのメルアドはユニークにしているのでwhereでその判定をして、んで「よし!ユニークなメルアドだな!」となれば実際にUserテーブルのメルアドをnew_mailに格納した新しいメールアドレスに変更して終了。
一応、わたしはその後のredirect処理でログアウトさせてもう一度新しいメールアドレスでログインしなおすようにしています。やらなくてもいいかもしれませんがメールアドレス変更したならログインしなおすのがユーザーとしても自然な流れかなーと思い、そうしてます。
また、今回はやってませんが、メールアドレス再登録が済んだタイミングでemail_verified_atの値も更新してもいいかもしれません。どっちでもいいと思います。
これにてlaravelでのメールアドレス変更機能実装終了!お疲れさまでした。
おまけ:laravelのメール認証の仕組み
laravelで普通にメール認証をすると、
- VerifyEmailっていうノーティフィケーションが呼ばれる
- その中でユーザーID(getKey)とメールアドレス(getEmailForVerification)を使って署名付きURLが作られる
- んでメールが送られる
- ユーザーがメールをクリックする
- ユーザーに送ったURLに日付有効期限、ユーザーID、メールアドレスの情報がハッシュされて入ってるんで、それとデータベースのuser情報とをチェック(EmailVerificationRequest内でgetKeyとgetEmailForVerification)
- 合ってたら「$req->fulfill()」でemail_verified_atに値を入力
ていう流れになります。
結局署名付きURLをVerifyEmail内で作る際はgetKeyとgetEmailForVerificationが呼ばれ、実際に照合する際もEmailVerificationRequest内でgetKeyとgetEmailForVerificationが呼ばれてます。
なので上で紹介した方法のように、getEmailForVerification(MustVerifyEmail.phpに実態があります、場所は後述)をオーバーライドしてあげて、場合によってメール認証に使うメルアドを変更できればOKという感じ。
結局、メール認証に使ってる署名付きURLを作る際もgetEmailForVerification、実際にそのリンクの有効性を確認するのもgetEmailForVerificationを呼んで、認証に使うメルアドを取得してるんで、getEmailForVerification関数内を書き換えてあげるのが一番laravelの元のメール認証そのままにメール再設定機能を実装できると思います。
おまけ:ファイルの場所
laravelってイルミネートとかいってそのファイルの場所わかりづらいですよね。
メール認証にlaravelが使ってるファイルの場所を書いておきますね。
VerifyEmail.phpの場所
実際にメール認証のための処理が書かれているVerifyEmail.phpは「\vendor\laravel\framework\src\Illuminate\Auth\Notifications\VerifyEmail.php」にあります。
MustVerifyEmail.phpの場所
email_verified_atのnull判定やメルアドの取得など、ある意味ラッパー関数?のようになってるMustVerifyEmail.phpは「\vendor\laravel\framework\src\Illuminate\Auth\MustVerifyEmail.php」にあります。
EmailVerificationRequest.phpの場所
メール認証でその署名付きURLが正しいかどうか判定しているEmailVerificationRequest.phpは「vendor/laravel/framework/src/Illuminate/Foundation/Auth/EmailVerificationRequest.php」にあります。
ちなみにですが「$req->fulfill();」でなんかすごい処理してそうですがただemail_verified_atの値を埋めてるだけって言うのはなんか面白いですよね。実際にはその前のバリデーション部分で確認終わってるっていう。
色々あるメルアド変更機能の実装方法
ここから下は参考資料的な内容なので読みたい人だけ読んでみてくださいね。
今回laravelでメールアドレス変更機能を作るに際しかなりいろいろな手法があるのがわかりました。
email_verified_atをnullにする
例えばマイページのユーザー情報編集画面で、メールアドレスを変更された場合、その処理でもうuserテーブルのメールアドレスを新しいメールアドレスに書き換え、んで「email_verified_at」の値をnullにしちゃう方法。
laravelは「email_verified_at」がnullかどうかで「auth」なのか「verified」なのか判定してるだけなので、「email_verified_at」をnullに手動でしちゃえば、またlaravelのメール認証機能がそのまま復活します。
めっちゃシンプルにメールアドレス変更機能が作れるんですが、一個だけ難点があって、ユーザーが間違ったメールアドレスに変更しちゃった場合、もうそのユーザーはログインが一生無理になるコト。userテーブルが間違ったメルアドに変わっちゃってるので。
それに対処するために「verified」ではなく「auth」だけの状態でもメールアドレス再設定だけはできるようにする、というのもあるんですが、これをしたとしても、例えばそのユーザーがなにかの拍子にログイン状態を抜けてしまうともう完全に詰みになります。
userテーブルには間違ったメールアドレスが登録されいるので、ログインすることも認証メールを受け取る事も、またメールアドレスを再設定することも何もできない状態になります。
最初この方法を採用しようかと考えてたんですが、上記に気づき辞めました。
もう自分で別途notifyを作る方法
ググった中で一番よく出てきたのがこの方法。
laravelの用意するメール認証機能の別に、自分でnotifyを作ってその中でゴリゴリ書いていく。
これでもよかったんですが、notify作って署名付きURLを自分で作って…、、とlaravelがあらかじめ用意した経路と違う経路を通るのがちょっと不安で。というか保守性を考えるとできる限りすでに実装されている経路をそのまま使いたいっていうのは至極当然なことだと思います。
なので、できればlaravelが用意してるメール認証機能をそのまま使いたいかなーって。
createUrlUsingコールバックを使う
これはググって出てきたわけじゃないんですが、ソースコード見てるうちにこれでもできるかもなー、と思った方法。
laravelはメール認証機能の時の署名付きURLを作る部分で「あ、もしよかったら違うやり方も受け付けるよー!」とコールバック関数を受け付けてます。
そもそも上でも書いた通りlaravelはメール認証の署名付きURL作成に、そのユーザーのIDとメールアドレスを使ってます(あとそのURLの有効時間を有限にするため日付情報も)。
\vendor\laravel\framework\src\Illuminate\Auth\Notifications\VerifyEmail.php
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()),
]
);
}
「$notifiable」にはそのモデルが入ってるので、userの場合はuserになります。
通知を送っているクラスのインスタンスである、$notifiableインスタンスを引数に受け取りますhttps://readouble.com/laravel/10.x/ja/notifications.html
んで「$notifiable->getKey()」はuserのプライマリーキーを取得してます。
さらに「$notifiable->getEmailForVerification()」は下記の通りuserのemailカラムの値を取得しています。
てことでこのコールバックでsha1()関数内のemail部分を変更できればなんかいけそう!ですよね。
\vendor\laravel\framework\src\Illuminate\Auth\MustVerifyEmail.php
public function getEmailForVerification()
{
return $this->email;
}
んでコールバックの話に戻りますが、createUrlUsingでURL生成用の関数を自分で定義できます。やり方はちょっと違っていて同じく「VerifyEmail.php」に用意されているコールバックである「toMailUsing」の例になりますが、laravel公式ドキュメントによると、「App\Providers\AuthServiceProvider」クラスの「boot」内で書けばいいみたいです。
ここでemailを設定する際に、新しいメルアドがあるならそっちを変数に使えばいいのかなーと思います。やってないんでわかりませんが。
ここまで考えて、「あれ?んじゃcreateUrlUsingでコールバック設定するよりgetEmailForVerificationをオーバーライドする方が楽なんじゃ?」と思ったという感じです。
まとめ
ここではlaravelでメールアドレス再設定を一番簡単に実装する方法を紹介しました。
調べているといろいろ方法があるんですが、個人的には「getEmailForVerification」をオーバーライドする方法が一番簡単だなーと思います。あらかじめlaravelが用意してくれている経路をそのまま流用できるので安心なのがいい所。
参考にしてみてくださいね。
あ、あとソースコードを載せるにあたって、わかりやすいように自分の環境とは違う変数名とかに一部変更しています。なのでもしかすると、まるまるコピペしただけだとタイピングミスで構文エラーとかが出るかも(複数形のsがなかったりemailとmailを間違ったり…)。動かないんだけど、ここ打ち間違いですよ、というのがあれば下記コメント欄より報告いただけると幸いです。
【おしらせ、というか完全なる宣伝】
文体がもうぜんぜん適当すぎてあれだけどものすごい自由に書いてるブログ「檸檬だくだく」もよろしく.寝る前に読める恐ろしくくだらないやつです.
こんなにも一ミリも目を引かれないタイトルを取り扱ってます: ココア20g / ハイチュウとかってさ / なぜ米と小麦を食べようと思ったのかの謎 /