【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ミドルウェアを適用します。しっかりルーティングの階層化もしておきましょう。

routes/web.php
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導入時の例ですが、リンクだけ繋がっていれば後は何でも良いです

先にSettingsControllerindexメソッドを作成し、

public function index()
{
    $user = Auth::user();
    return view('settings', compact('user')); 
}

で表示だけでもできるようにすることを推奨します。

resources/views/layouts/navigation.php
<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>
resources/views/settings.blade.php
<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>
resources/views/components/app-contents.blade.php
<!-- ただの見た目調整用のコンポーネント -->
<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の重複をチェックします。重複しているということは、emailusersテーブルを検索したらヒットするということです。ここで、メアドを変更していない場合に引っかからないよう、自分の分は除外します。

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の形式が正しくありません。',
            ],
            // ...
        ];
    }
}

あとはauthorizetrueを返すように変更するのを忘れないようにしておく必要があります。

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からモデルを取得するべきです。

取得したら、nameemailを入れて更新します。ここで、更新後は元のページに戻りますが、戻った際に成功したというメッセージを表示する必要があります。普通に$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テーブルは、emailuniqueが設定されています。そのため、同時に同じメールアドレスに変更するリクエストがあっても、メールアドレスが重複することはありません。

メールアドレスの変更の認証機能

メールアドレス変更時は、そのメールアドレスが存在するかの再確認を行うことがよくあります。そこで、新規登録の場合と同じように認証メールを送信して新しいメールアドレスを確かめる機能を実行しましょう。

事前にUserモデルでclass User extends Authenticatable implements MustVerifyEmailのようにMustVerifyEmailをimplementする必要があります。

まずは必要なものを列挙しましょう。

  • ユーザ情報更新画面 (上の節の内容で用意)
  • 更新時のイベントとリスナーの用意
  • Email更新時にメールアドレス認証の状態をリセットして、新しいアドレスにメールを送信する処理
  • 認証用ページ用画面 (Breeze使用時は不要)

メールアドレス認証の状態をリセット

メールアドレス認証の状態をリセットするには、users.email_verified_atnullを入れるだけです。

$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が呼ばれるようにすると良さそうですが、SendEmailVerificationNotificationRegisterイベントしか受け付けません。メールアドレス更新にRegisterを使用するのもおかしいので、同じ実装のものを用意します。

作ったらEventServiceProviderでイベントとリスナーを紐付けます。

php artisan make:event UpdateEmailEvent
php artisan make:listener UpdateEmailVerificationNotification
app/Events/UpdateEmailEvent.php
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;
    }

    // ...
}
app/Listeners/UpdateEmailVerificationNotification.php
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();
        }
    }
}
app/Providers/EventServiceProvider.php
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');
});

コントローラでは、idhashの検証にはEmailVerificationRequestを用います。このRequestを通った時点でハッシュの検証が済んでいるので、後は更新処理をかければ完了です。

app/Http/Controllers/Auth/VerifyEmailController.php
// コードは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_atnullを入れることでLaravelの機能を用いて認証できることを知っておくと楽になります。

laravel user data thumb

役に立ったらシェアしよう!