【Laravel】 ユーザ情報の取得と更新の方法
Webアプリでは、しばしばユーザ認証機能があります。アカウントを作成し、Emailを認証してログインして使用します。ではログインしているユーザの情報を更新するにはどのように組めばよいのでしょうか。
本記事では、ログインの仕組みを把握した上で、どうデータを操作するかなどの説明になります。また、Laravel Breezeを導入してそれを理解した状態のほうが進めやすいと思います。Breezeではなく自前実装でもログイン後の処理に大差は無いので問題ありません。
アカウント登録やログインの仕組み、ミドルウェアを用いた制限などに関しては、下の記事を参考にしてください。
目次
ユーザ情報を取得する
ログインしたユーザの情報は、Auth::user()
で取得できます。
use Illuminate\Support\Facades\Auth; class UserController extends Controller { public function index() { $user = Auth::user(); return view('user.index', compact('user')); } }
取得できるユーザ情報は、User
モデルの内容そのままです。つまりusers
テーブルの該当ユーザの全情報が入っています。ユーザ個人の特定には、id
を用いることがほとんどです。
中身がほぼモデルなので、モデルと同じような感覚でデータの取得ができます。
<!-- bladeからAuthを呼び出して取得 --> <p>{{ Auth::user()->name }}</p> <!-- controllerなどから渡された値から取得 --> <p>{{ $user->email }}</p>
IDだけ取得したい場合、Auth::id()
が楽です。
$userId = Auth::id();
中身を確認すると@return \Illuminate\Contracts\Auth\Authenticatable|null
とあります。このクラスをUser
モデルが継承しています。
Auth::user()
で取得されるのはUser
モデルです。ユーザ情報のテーブルがusers
以外にしたい場合は、config/auth.php
で切り替えます。
基本的にダウンキャストはエラーの温床なので避けるべきですが、設定でどのモデルが取得されるかが保証されています。しかし入力補完が効かないので、基本的には情報の取得以外は非推奨です。
ユーザ情報を更新する
まずはユーザを更新するための画面構成を考え、必要なものを列挙していきます。
- Userモデル (元からある)
- 更新用フォームと、データの送信先のルーティング
- 更新用フォームを含む画面
- ユーザ情報に関するRequestクラス (推奨)
- 更新に使うコントローラのアクション (メソッド)
それぞれを作成していきましょう。
順不同ですが、おそらくこの順番がテストしながらの進行がしやすいと思います。
また、RequestやControllerなど、先に用意しておくとスムーズになります。
ルーティングの作成
まずはルーティングを作成し、画面の表示や送信の準備をしていきます。
ユーザ情報の設定画面はサイトによって少しパスがバラバラです。人気のWebサービスならどれを参考にしても良いと思いますが、今回は説明を単純にするため、ルート直下のsettings
に全て置きます。
ここで、設定画面にはログイン済みユーザ以外はアクセスできないようにするため、auth
ミドルウェアを適用します。しっかりルーティングの階層化もしておきましょう。
use App\Http\Controllers\SettingsController; Route::middleware(['auth'])->group(function () { Route::get('/dashboard', function () { return view('dashboard'); })->name('dashboard'); // 設定関連のページのルーティング Route::name('settings.')->prefix('/settings')->group(function () { Route::get('/', [SettingsController::class, 'index'])->name('index'); Route::put('/', [SettingsController::class, 'update'])->name('update'); }); });
先にphp artisan make:controller SettingsController
しておくと、use
が入力補完で自動で入るので便利です。
ルーティングの書き方は下のページで説明しています。
更新用フォームを含む画面
画面ということはビューです。見た目は何でも良いので、フォームさえあれば十分です。
下の例はBreeze導入時の例ですが、リンクだけ繋がっていれば後は何でも良いです
先にSettingsController
にindex
メソッドを作成し、
public function index() { $user = Auth::user(); return view('settings', compact('user')); }
で表示だけでもできるようにすることを推奨します。
<nav x-data="{ open: false }" class="bg-white border-b border-gray-100"> <!-- ... --> <x-slot name="content"> <form method="POST" action="{{ route('logout') }}"> <!-- ... --> </form> <!-- これを追加してリンクを追加。Breezeでない場合は普通のaタグでok --> <div> <x-dropdown-link :href="route('settings.index')"> Settings </x-dropdown-link> </div> </x-slot> </nav>
<x-app-layout> <x-slot name="header"> UserSettings </x-slot> <x-app-contents> <p>user {{ $user->email }}</p> <!-- ユーザ情報送信フォーム --> <form action="{{ route('settings.update') }}" method="post" class="px-8 pt-6 pb-8 mb-4"> <!-- 今回は情報の更新でPUTを使いたいため、ここでmethodを指定 --> @method('PUT') @csrf <div class="mb-4"> <label for="name" class="text-sm block">UserName</label> <input type="text" name="name" id="name" value="{{ old('name') ?? $user->name }}"> </div> <div class="mb-4"> <label for="email" class="text-sm block">Email</label> <input type="email" name="email" id="email" value="{{ old('email') ?? $user->email }}"> </div> <input type="submit" value="Submit" class="bg-blue-500 hover:bg-blue-700 text-white font-bold py-2 px-4 rounded focus:outline-none focus:shadow-outline"> </form> </x-app-contents> </x-app-layout>
<!-- ただの見た目調整用のコンポーネント --> <div class="py-12"> <div class="max-w-7xl mx-auto sm:px-6 lg:px-8"> <div class="bg-white overflow-hidden shadow-sm sm:rounded-lg"> <div class="p-6 bg-white border-b border-gray-200"> {{ $slot }} </div> </div> </div> </div>
tailwind用のclassは省略しています。HTMLがかなり肥大化するために見づらくなる、構造化できないなどデメリットが多すぎるので個人的にはあまりおすすめできません。
もしフォーム部分の見た目を整えたい場合、submit
以外のinput
タグに対してclass="shadow appearance-none border rounded w-full py-2 px-3 text-gray-700 leading-tight focus:outline-none focus:shadow-outline"
を加えてください。
{{ old('name') ?? $user->name }}
というコードは、もし送信失敗などでセッションに入力内容が保持されていた場合はその値を使用し、なければユーザ情報の値を入れる、というものです。
??
はnull合体演算子と呼ばれるもので、左辺がnull
以外なら左辺の値を、そうでなければ右辺の値を返す演算子です。
settings.blade.php
では、コントローラから渡されたAuth::user()
の情報を利用しています。x-app-layout
はBreezeを使用していない場合は無いと思いますので、省略でも大丈夫です。
これで画面が作成できました。
ユーザ情報に関するRequestクラス
ユーザ情報を送信する際はいくつかのバリデーションを行う必要があります。例えば、既に登録されているメールアドレスかどうか、そもそもメールアドレスとして正しい形式かどうか、必要な値は入っているかなどです
フロント側では開発者ツールなどで自由にHTMLを変更して送信できるので、必ずサーバ側でチェックを行いましょう。今回は簡易的なチェックのみ用意します。
まずは
php artisan make:request UpdateUserRequest
でユーザ情報更新用のリクエストを作ります。作ったらrules
を設定しバリデーションしましょう。
public function rules() { return [ 'email' => ['required', 'email'], 'name' => ['required', 'between:4,64'], ]; }
あとはEmailの重複をチェックします。重複しているということは、email
でusers
テーブルを検索したらヒットするということです。ここで、メアドを変更していない場合に引っかからないよう、自分の分は除外します。
public function rules() { return [ 'email' => ['required', 'email', function ($name, $item, $fail) { // もし既に使用されているemailなら弾く if (count(User::where([ ['email', $item], ['id', '<>', Auth::id()] ])->get())) { $fail('The email address is already in use.'); } }], 'name' => ['required', 'between:4,64'], ]; }
このような独自のバリデーションの書き方は
ちょっとif文が長いですが、要はemail = $item AND id <> ユーザID
としています。email
で検索しつつ、自分のIDは除外しているということです。あとは検索結果の件数が0件かどうかを見ます。
ユーザ名は適当に4~64文字としました。適当で良いです。
メッセージをカスタマイズしたい場合はmessages
メソッドで調整します。
class UpdateUserRequest extends FormRequest { // ... public function messages() { return [ 'email' => [ 'required' => 'Emailは必須です', 'email' => 'Emailの形式が正しくありません。', ], // ... ]; } }
あとはauthorize
でtrue
を返すように変更するのを忘れないようにしておく必要があります。
class UpdateUserRequest extends FormRequest { public function authorize() { return true; } // ... }
更新に使うコントローラのアクション
処理の本体と言えるアクションの設定を行います。ルーティングでは、SettingsController
を使用しました。まだコントローラを作成していない場合は作成します。
php artisan make:controller SettingsController
作成したら、update
メソッドを実装して更新処理を作っていきます。送信されるリクエストは、事前に作っておいたUpdateUserRequest
を使用します。
更新する場合は、Auth::id()
から取得したUser
モデルを用いて更新します。
use App\Http\Requests\UpdateUserRequest; class SettingsController extends Controller { public function index() { $user = Auth::user(); return view('settings', compact('user')); } public function update(UpdateUserRequest $request) { $user = User::find(Auth::id()); // ... } }
Auth::user()
は実質User
モデルなのでsave
はできるのですが、Auth::user()
が返す値の型はAuthenticatable
であり、save
などのインターフェースは定義されていません。
ダウンキャストに抵抗がなければ使っても良いですが、入力補完が効かないため、一度User
からモデルを取得するべきです。
取得したら、name
とemail
を入れて更新します。ここで、更新後は元のページに戻りますが、戻った際に成功したというメッセージを表示する必要があります。普通に$message
変数を渡すという手もありますが、セッションを用いたフラッシュメッセージも方法の1つです。
// ユーザのモデル取得 try { $user = User::find(Auth::id()); } catch (Exception $e) { Session::flash('error_message', 'Server error.'); return view('settings', compact('user')); } // 値更新 $user->name = $request->input('name'); $user->email = $request->input('email'); // 保存処理 try{ $user->save(); Session::flash('flash_message', 'Successful update.'); } catch (Exception $e) { Session::flash('error_message', 'Server error.'); } return view('settings', compact('user'));
{{-- 成功時のメッセージ --}} @if (session('flash_message')) <div class="p-4 mb-4 text-sm text-green-700 bg-green-100 rounded-lg dark:bg-green-200 dark:text-green-800" role="alert"> {{ session('flash_message') }} </div> @endif {{-- 重複時のメッセージ --}} @if (session('error_message')) <div class="p-4 mb-4 text-sm text-red-700 bg-red-100 rounded-lg dark:bg-red-200 dark:text-red-800" role="alert"> {{ session('error_message') }} </div> @endif
// どちらでも良い session()->flash('flash_message', 'Successful update.'); use Illuminate\Support\Facades\Session; Session::flash('flash_message', 'Successful update.');
これで更新機能は完成です。
Laravelのデフォルトのusers
テーブルは、email
にunique
が設定されています。そのため、同時に同じメールアドレスに変更するリクエストがあっても、メールアドレスが重複することはありません。
メールアドレスの変更の認証機能
メールアドレス変更時は、そのメールアドレスが存在するかの再確認を行うことがよくあります。そこで、新規登録の場合と同じように認証メールを送信して新しいメールアドレスを確かめる機能を実行しましょう。
事前にUser
モデルでclass User extends Authenticatable implements MustVerifyEmail
のようにMustVerifyEmail
をimplementする必要があります。
まずは必要なものを列挙しましょう。
- ユーザ情報更新画面 (上の節の内容で用意)
- 更新時のイベントとリスナーの用意
- Email更新時にメールアドレス認証の状態をリセットして、新しいアドレスにメールを送信する処理
- 認証用ページ用画面 (Breeze使用時は不要)
メールアドレス認証の状態をリセット
メールアドレス認証の状態をリセットするには、users.email_verified_at
にnull
を入れるだけです。
$user->name = $request->input('name'); if ($user->email !== $request->input('email')) { $user->email_verified_at = null; } $user->email = $request->input('email');
認証メールの送信
入れたら、認証メールを送信します。アカウント作成時はEventServiceProvider
を見ると、
protected $listen = [ Registered::class => [ SendEmailVerificationNotification::class, ], ];
とあります。SendEmailVerificationNotification::class
が呼ばれるようにすると良さそうですが、SendEmailVerificationNotification
はRegister
イベントしか受け付けません。メールアドレス更新にRegister
を使用するのもおかしいので、同じ実装のものを用意します。
作ったらEventServiceProvider
でイベントとリスナーを紐付けます。
php artisan make:event UpdateEmailEvent php artisan make:listener UpdateEmailVerificationNotification
namespace App\Providers; use App\Models\User; // ... class UpdateEmailEvent { use Dispatchable, InteractsWithSockets, SerializesModels; private $user = null; public function __construct(User $user) { $this->user = $user; } public function getUser() { return $this->user; } // ... }
namespace App\Listeners; use App\Events\UpdateEmailEvent; use Illuminate\Contracts\Auth\MustVerifyEmail; // ... class UpdateEmailVerificationNotification { public function __construct() { // } public function handle(UpdateEmailEvent $event) { // SendEmailVerificationNotificationの実装をほぼコピペ $user = $event->getUser(); if ($user instanceof MustVerifyEmail && ! $user->hasVerifiedEmail()) { $user->sendEmailVerificationNotification(); } } }
use App\Events\UpdateEmailEvent; use App\Listeners\UpdateEmailVerificationNotification; // ... class EventServiceProvider extends ServiceProvider { protected $listen = [ Registered::class => [ SendEmailVerificationNotification::class, ], UpdateEmailEvent::class => [ UpdateEmailVerificationNotification::class, ], ]; // ... }
イベントとプロバイダの設定を済ませたら、早速メールを送信する処理を書きます。イベントを送信するだけですが、メールアドレスが変化したときだけ送信になるようにしておきます。
このとき、メール送信でトランザクションを行うことで、メール送信で例外排出された際にもロールバックできます。
// ... // フラグを持っておく $doUpdateEmail = false; if ($user->email !== $request->input('email')) { $user->email = $request->input('email'); $user->email_verified_at = null; $doUpdateEmail = true; } try{ DB::transaction(function () use(&$user, $doUpdateEmail) { $user->save(); if ($doUpdateEmail) { event(new UpdateEmailEvent($user)); } }); Session::flash('flash_message', 'Successful update.'); } catch (Exception $e) { Session::flash('error_message', 'Server error.'); } // ...
transaction
内に色々入れると確かにスッキリするのですが、try-catch
を狭くしたほうが良いのと同じ理由で意図的に外に出しています。
作ったらメールが送信されるかチェックします。Breezeを導入している場合、あとはメールをクリックしたらusers.email_verified_at
が更新されているか確認して完了です。
Laravel sail
で構築時は、http://localhost:8025
にアクセスするとローカル開発用のメール受信箱が見えます。
Breeze
を利用している場合、メールアドレス認証用ページは最初からauth
ミドルウェアが適用されています。
認証用画面の用意
Breeze使用時は追加不要です。
Laravelは、認証用メールが組み込まれています。そこの仕様に合わせてルート設定を行います。あとは認証処理をするコントローラが必要です。認証後は適当にホームなどに飛ばせばよいため、画面は不要です。
更新時はログインした状態で行うためにauth
ミドルウェアと、署名付きURLを扱うのに便利なsigned
ミドルウェアを使用します。
Route::middleware('auth')->group(function () { Route::get('verify-email/{id}/{hash}', [VerifyEmailController::class, '__invoke']) ->middleware(['signed', 'throttle:6,1']) ->name('verification.verify'); });
コントローラでは、id
とhash
の検証にはEmailVerificationRequest
を用います。このRequest
を通った時点でハッシュの検証が済んでいるので、後は更新処理をかければ完了です。
// コードはBreezeの導入で作成されるコードを使用 namespace App\Http\Controllers\Auth; use App\Http\Controllers\Controller; use App\Providers\RouteServiceProvider; use Illuminate\Auth\Events\Verified; use Illuminate\Foundation\Auth\EmailVerificationRequest; class VerifyEmailController extends Controller { public function __invoke(EmailVerificationRequest $request) { if ($request->user()->hasVerifiedEmail()) { return redirect()->intended(RouteServiceProvider::HOME.'?verified=1'); } if ($request->user()->markEmailAsVerified()) { event(new Verified($request->user())); } return redirect()->intended(RouteServiceProvider::HOME.'?verified=1'); } }
まとめ
ユーザ情報の取得や更新には、Auth
ファサードを用います。Auth::user()
でUser
モデルが取得でき、Auth::id()
でidが取得できます。
メールアドレス更新時の認証では、users.email_verified_at
にnull
を入れることでLaravelの機能を用いて認証できることを知っておくと楽になります。
