Muhammad Yasir Amin

Loading websiteโ€ฆ

More from Muhammad Yasir Amin

Build Along With Me

Practical walkthroughs on Laravel, React, APIs and deployment workflows.

Subscribe on YouTube

Laravel 13 Update User Profile Tutorial: Name, Email, Password and Avatar

๐Ÿ› ๏ธ Laravel Tutorials5 min read

Laravel 13 Update User Profile Tutorial: Name, Email, Password and Avatar

Muhammad Yasir Amin

Muhammad Yasir Amin

Senior Full Stack Engineerโ€ขNov 12, 2026
Laravel 13 Update User Profile Tutorial: Name, Email, Password and Avatar

To let a user update their profile in Laravel 13, create an authenticated GET and PATCH route, validate the input with a form request that uses Rule::unique('users')->ignore($this->user()) for the email, and save only validated fields on $request->user(). Put password changes in a separate form that requires the current password.

Starter kits include profile pages, but many apps have custom user fields or no starter kit at all. This tutorial builds the feature by hand so every piece is visible.

Prerequisites

  • Laravel 13 with PHP 8.3 or newer.
  • Working login with the auth middleware.
  • An avatar_path nullable string column on users if you want avatars.
  • The public disk linked with php artisan storage:link.

Step 1: Define the routes

CSS
use App\Http\Controllers\ProfileController;

Route::middleware('auth')->group(function () {
    Route::get('/profile', [ProfileController::class, 'edit'])->name('profile.edit');
    Route::patch('/profile', [ProfileController::class, 'update'])->name('profile.update');
    Route::put('/profile/password', [ProfileController::class, 'updatePassword'])->name('profile.password');
});

There is no user id in the URL. The controller always works on the signed-in user, so one user can never edit another by changing a number.

Step 2: Validate profile details with a form request

Code
php artisan make:request UpdateProfileRequest
JavaScript
<?php

namespace App\Http\Requests;

use Illuminate\Foundation\Http\FormRequest;
use Illuminate\Validation\Rule;

class UpdateProfileRequest extends FormRequest
{
    public function authorize(): bool
    {
        return $this->user() !== null;
    }

    public function rules(): array
    {
        return [
            'name' => ['required', 'string', 'max:255'],
            'email' => [
                'required',
                'string',
                'lowercase',
                'email',
                'max:255',
                Rule::unique('users', 'email')->ignore($this->user()),
            ],
            'avatar' => ['nullable', 'image', 'mimes:jpg,jpeg,png,webp', 'max:2048'],
        ];
    }
}

Without ignore(), saving the form with an unchanged email would fail because that email already exists: it belongs to the same user. Passing the user model tells the rule to skip that row.

Step 3: Update the profile in the controller

JavaScript
<?php

namespace App\Http\Controllers;

use App\Http\Requests\UpdateProfileRequest;
use Illuminate\Http\RedirectResponse;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Hash;
use Illuminate\Support\Facades\Storage;
use Illuminate\Validation\Rules\Password;
use Illuminate\View\View;

class ProfileController extends Controller
{
    public function edit(Request $request): View
    {
        return view('profile.edit', ['user' => $request->user()]);
    }

    public function update(UpdateProfileRequest $request): RedirectResponse
    {
        $user = $request->user();
        $user->fill($request->safe()->only(['name', 'email']));

        if ($user->isDirty('email')) {
            $user->email_verified_at = null;
        }

        if ($request->hasFile('avatar')) {
            $newPath = $request->file('avatar')->store('avatars', 'public');

            if ($user->avatar_path) {
                Storage::disk('public')->delete($user->avatar_path);
            }

            $user->avatar_path = $newPath;
        }

        $user->save();

        return redirect()->route('profile.edit')->with('status', 'Profile updated.');
    }

    public function updatePassword(Request $request): RedirectResponse
    {
        $validated = $request->validateWithBag('password', [
            'current_password' => ['required', 'current_password'],
            'password' => ['required', 'confirmed', Password::defaults()],
        ]);

        $request->user()->update([
            'password' => Hash::make($validated['password']),
        ]);

        return redirect()->route('profile.edit')->with('status', 'Password changed.');
    }
}

Three details are worth pointing out. $request->safe()->only() takes values from validated data only, so an extra field like is_admin in the request is ignored. When the email changes, email_verified_at is cleared so the user must verify the new address. The old avatar is deleted only after the new one is stored.

Why is the password form separate?

The current_password rule checks the entered value against the logged-in user's hash. Asking for it protects accounts left signed in on a shared computer. Keeping it in its own form with validateWithBag('password', ...) means its errors do not mix with profile errors on the same page.

Step 4: Build the Blade form

Shell
<form method="POST" action="{{ route('profile.update') }}" enctype="multipart/form-data">
    @csrf
    @method('PATCH')

    <label>Name</label>
    <input name="name" value="{{ old('name', $user->name) }}" required>
    @error('name') <p class="error">{{ $message }}</p> @enderror

    <label>Email</label>
    <input type="email" name="email" value="{{ old('email', $user->email) }}" required>
    @error('email') <p class="error">{{ $message }}</p> @enderror

    <label>Avatar</label>
    <input type="file" name="avatar" accept="image/*">
    @error('avatar') <p class="error">{{ $message }}</p> @enderror

    <button type="submit">Save</button>
</form>

<form method="POST" action="{{ route('profile.password') }}">
    @csrf
    @method('PUT')
    <input type="password" name="current_password" autocomplete="current-password">
    @error('current_password', 'password') <p class="error">{{ $message }}</p> @enderror
    <input type="password" name="password" autocomplete="new-password">
    <input type="password" name="password_confirmation" autocomplete="new-password">
    @error('password', 'password') <p class="error">{{ $message }}</p> @enderror
    <button type="submit">Change password</button>
</form>

The enctype attribute is required for file uploads. old() keeps the user's input after a validation error.

Common mistakes

  • Using $request->all() with update(). It invites mass assignment problems. Use validated data.
  • Forgetting ignore() on the unique rule. Users see "The email has already been taken" for their own email.
  • Storing a plain password. The default Laravel 13 User model has a 'password' => 'hashed' cast, which hashes plain values for you and leaves already hashed values alone. If your model has no such cast, you must call Hash::make() yourself.
  • Missing enctype. The avatar arrives empty and validation passes because the field is nullable.

How do you verify it works?

JavaScript
<?php

use App\Models\User;

test('a user can update their name and email', function () {
    $user = User::factory()->create();

    $this->actingAs($user)
        ->patch('/profile', ['name' => 'New Name', 'email' => 'new@example.com'])
        ->assertRedirect('/profile')
        ->assertSessionHasNoErrors();

    expect($user->fresh())
        ->name->toBe('New Name')
        ->email->toBe('new@example.com')
        ->email_verified_at->toBeNull();
});

test('the current password is required to change it', function () {
    $user = User::factory()->create();

    $this->actingAs($user)
        ->put('/profile/password', [
            'current_password' => 'wrong-password',
            'password' => 'N3w-Strong-Passw0rd!',
            'password_confirmation' => 'N3w-Strong-Passw0rd!',
        ])
        ->assertSessionHasErrorsIn('password', 'current_password');
});

Where should avatar files be stored?

The example uses the public disk because avatars are shown to other users and do not need protection. Display them with Storage::url($user->avatar_path) and fall back to a default image when the column is null. If you later move files to S3, only the disk name changes. For very large uploads, resize the image to a fixed size before storing it, so every avatar loads quickly in lists and comments.

Profile pages are also a good place to log changes. An updated observer on User that records when the email changed helps support staff answer "I never changed my email" tickets.

FAQ

How do I let users delete their account?

Add a DELETE route that validates current_password, logs the user out, deletes the record, and invalidates the session.

Should profile updates log out other devices?

For password changes, yes. Call Auth::logoutOtherDevices($password) and make sure the auth.session middleware is applied to the routes you want to protect.

Can I use this with an API?

Yes. The same form request works with Sanctum; return JSON instead of redirects.

References

Article Index & Topic Tags
LaravelAuthenticationValidationProfile

YouTube Channel

Build Along With Me

Practical walkthroughs on Laravel, React, APIs and deployment workflows.

Muhammad Yasir Amin
Available for Contracts
Author & Solutions Architect

Senior Full Stack Engineer

Independent Senior Full Stack Engineer working on Laravel, React and Next.js applications, APIs and delivery workflows.

Laravel & PHP ApplicationsReact & Next.js InterfacesREST APIs & IntegrationsDatabase Queries & DebuggingDocker Development WorkflowsClear Scope & Handover
Discuss Your Next Step

What Does Your Application Need Next?

Share your current system, the problem you want to solve and what a useful result looks like. We can identify the right starting point and agree on a practical scope.

Technical Audit

Find the Next Useful Fix

Application Development

Scoped, Reviewable Milestones

Ongoing Support

Agreed Capacity and Priorities

Share your goals, current stack and requirements before the call.