Laravel 12 Users Profile CRUD (With Image Upload)
A full step-by-step guide using Laravel, Bootstrap 5, and image handling — styled with your custom class system.
1. Create Laravel Project
composer create-project laravel/laravel users-profile-app
cd users-profile-app
2. Set Up Model & Migration
php artisan make:model Profile -m
Edit the migration file to add the necessary fields:
📂 Migration File: database/migrations/xxxx_xx_xx_create_profiles_table.php
public function up(): void
{
Schema::create('profiles', function (Blueprint $table) {
$table->id();
$table->string('first_name');
$table->string('last_name');
$table->string('email')->unique();
$table->string('password');
$table->string('image')->nullable(); // store image path
$table->timestamps();
});
}
Run the migration using the following command:
php artisan migrate
📄 Model File: app/Models/Profile.php
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Foundation\Auth\User as Authenticatable;
class Profile extends Authenticatable
{
use HasFactory;
protected $fillable = [
'first_name',
'last_name',
'email',
'password',
'image',
];
protected $hidden = [
'password',
];
// Automatically hash password
public function setPasswordAttribute($value)
{
$this->attributes['password'] = bcrypt($value);
}
}
3. Run Migration
php artisan migrate
4. Controller with Image Upload
php artisan make:controller UsersProfileController
Implement functions: index, create, store, edit, update, destroy
5. Define Web Routes
Route::get('users', [UsersProfileController::class, 'index']); Route::get('users/create', [UsersProfileController::class, 'create']); Route::post('users', [UsersProfileController::class, 'store']); Route::get('users/{id}/edit', [UsersProfileController::class, 'edit']); Route::put('users/{id}', [UsersProfileController::class, 'update']); Route::get('users/{id}', [UsersProfileController::class, 'show']); Route::delete('users/{id}', [UsersProfileController::class, 'destroy']);
10. Full Controller Code
namespace App\Http\Controllers;
use Illuminate\Http\Request;
use App\Models\Profile;
use Illuminate\Support\Facades\File;
class UsersProfileController extends Controller
{
public function index()
{
$profiles = Profile::all();
return view('users.index', compact('profiles'));
}
public function create()
{
return view('users.create');
}
public function store(Request $request)
{
$request->validate([
'first_name' => 'required|string|max:100',
'last_name' => 'required|string|max:100',
'email' => 'required|email|unique:profiles,email',
'password' => 'required|min:6',
'image' => 'nullable|image|mimes:jpeg,png,jpg|max:2048',
]);
$imagePath = null;
if ($request->hasFile('image')) {
$fileName = time() . '_' . $request->file('image')->getClientOriginalName();
$request->file('image')->move(public_path('images/profiles'), $fileName);
$imagePath = 'images/profiles/' . $fileName;
}
Profile::create([
'first_name' => $request->first_name,
'last_name' => $request->last_name,
'email' => $request->email,
'password' => bcrypt($request->password),
'image' => $imagePath,
]);
return redirect()->route('users.index')->with('success', 'Profile created.');
}
public function edit($id)
{
$profile = Profile::findOrFail($id);
return view('users.edit', compact('profile'));
}
public function update(Request $request, $id)
{
$profile = Profile::findOrFail($id);
$request->validate([
'first_name' => 'required|string|max:100',
'last_name' => 'required|string|max:100',
'email' => 'required|email|unique:profiles,email,' . $id,
'password' => 'nullable|min:6',
'image' => 'nullable|image|mimes:jpeg,png,jpg|max:2048',
]);
if ($request->hasFile('image')) {
if ($profile->image && File::exists(public_path($profile->image))) {
File::delete(public_path($profile->image));
}
$fileName = time() . '_' . $request->file('image')->getClientOriginalName();
$request->file('image')->move(public_path('images/profiles'), $fileName);
$profile->image = 'images/profiles/' . $fileName;
}
$profile->first_name = $request->first_name;
$profile->last_name = $request->last_name;
$profile->email = $request->email;
if ($request->filled('password')) {
$profile->password = bcrypt($request->password);
}
$profile->save();
return redirect()->route('users.index')->with('success', 'Profile updated.');
}
public function destroy($id)
{
$profile = Profile::findOrFail($id);
if ($profile->image && File::exists(public_path($profile->image))) {
File::delete(public_path($profile->image));
}
$profile->delete();
return redirect()->route('users.index')->with('success', 'Profile deleted.');
}
public function show($id)
{
$profile = Profile::findOrFail($id);
return view('users.show', compact('profile'));
}
}
6. Blade Templates
layouts/app.blade.php— Layout with Bootstrap 5 + custom styles
// File: resources/views/layouts/app.blade.php
<!DOCTYPE html>
<html>
<head>
<title>@yield('title', 'Users Profile')</title>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<!-- Bootstrap 5 CDN -->
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css" rel="stylesheet">
<style>
.profile-image {
width: 60px;
height: 60px;
object-fit: cover;
border-radius: 50%;
}
</style>
</head>
<body>
<div class="container py-4">
<h2 class="mb-4 text-center">@yield('title', 'Users Profile')</h2>
@if(session('success'))
<div class="alert alert-success">{{ session('success') }}</div>
@endif
@yield('content')
</div>
<!-- Bootstrap JS -->
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/js/bootstrap.bundle.min.js"></script>
</body>
</html>
users/index.blade.php— List profiles
// File: resources/views/users/index.blade.php
<@extends('layouts.app')>
<@section('title', 'All User Profiles')>
<@section('content')>
<div class="mb-3 text-end">
<a href="{{ route('users.create') }}" class="btn btn-primary">+ Create New Profile</a>
</div>
<table class="table table-bordered table-hover">
<thead class="table-light">
<tr>
<th>#</th>
<th>Image</th>
<th>Full Name</th>
<th>Email</th>
<th style="width: 150px;">Actions</th>
</tr>
</thead>
<tbody>
@forelse($profiles as $index => $profile)
<tr>
<td>{{ $index + 1 }}</td>
<td>
@if($profile->image)
<img src="{{ asset($profile->image) }}" alt="image" class="profile-image">
@else
<span class="text-muted">N/A</span>
@endif
</td>
<td>{{ $profile->first_name }} {{ $profile->last_name }}</td>
<td>{{ $profile->email }}</td>
<td>
<a href="{{ route('users.edit', $profile->id) }}" class="btn btn-sm btn-warning">Edit</a>
<form action="{{ route('users.destroy', $profile->id) }}" method="POST" class="d-inline" onsubmit="return confirm('Delete this profile?')">
@csrf
@method('DELETE')
<button class="btn btn-sm btn-danger">Del</button>
</form>
</td>
</tr>
@empty
<tr><td colspan="5" class="text-center text-muted">No profiles found.</td></tr>
@endforelse
</tbody>
</table>
<@endsection>
users/create.blade.php— Add form
// File: resources/views/users/create.blade.php
<@extends('layouts.app')>
<@section('title', 'Create User Profile')>
<@section('content')>
<div class="container mt-4">
<h2 class="mb-4">Create User Profile</h2>
<form action="{{ route('users.store') }}" method="POST" enctype="multipart/form-data" class="card p-4 shadow-sm">
@csrf
<div class="mb-3">
<label class="form-label">First Name</label>
<input type="text" name="first_name" class="form-control" value="{{ old('first_name') }}" required>
@error('first_name') <small class="text-danger">{{ $message }}</small> @enderror
</div>
<div class="mb-3">
<label class="form-label">Last Name</label>
<input type="text" name="last_name" class="form-control" value="{{ old('last_name') }}" required>
@error('last_name') <small class="text-danger">{{ $message }}</small> @enderror
</div>
<div class="mb-3">
<label class="form-label">Email</label>
<input type="email" name="email" class="form-control" value="{{ old('email') }}" required>
@error('email') <small class="text-danger">{{ $message }}</small> @enderror
</div>
<div class="mb-3">
<label class="form-label">Password</label>
<input type="password" name="password" class="form-control" required>
@error('password') <small class="text-danger">{{ $message }}</small> @enderror
</div>
<div class="mb-3">
<label class="form-label">Profile Image</label>
<input type="file" name="image" class="form-control">
@error('image') <small class="text-danger">{{ $message }}</small> @enderror
</div>
<div class="text-end">
<button type="submit" class="btn btn-primary">Create Profile</button>
</div>
</form>
</div>
<@endsection>
users/edit.blade.php— Edit with image preview
// File: resources/views/users/edit.blade.php
<@extends('layouts.app')>
<@section('title', 'Edit User Profile')>
<@section('content')>
<form action="{{ route('users.update', $profile->id) }}" method="POST" enctype="multipart/form-data" class="card p-4 shadow-sm">
@csrf
@method('PUT')
<div class="mb-3">
<label class="form-label">First Name</label>
<input type="text" name="first_name" class="form-control" value="{{ old('first_name', $profile->first_name) }}" required>
@error('first_name') <small class="text-danger">{{ $message }}</small> @enderror
</div>
<div class="mb-3">
<label class="form-label">Last Name</label>
<input type="text" name="last_name" class="form-control" value="{{ old('last_name', $profile->last_name) }}" required>
@error('last_name') <small class="text-danger">{{ $message }}</small> @enderror
</div>
<div class="mb-3">
<label class="form-label">Email</label>
<input type="email" name="email" class="form-control" value="{{ old('email', $profile->email) }}" required>
@error('email') <small class="text-danger">{{ $message }}</small> @enderror
</div>
<div class="mb-3">
<label class="form-label">New Password <small class="text-muted">(leave blank to keep existing)</small></label>
<input type="password" name="password" class="form-control">
@error('password') <small class="text-danger">{{ $message }}</small> @enderror
</div>
<div class="mb-3">
<label class="form-label">Profile Image</label><br>
@if ($profile->image)
<img src="{{ asset($profile->image) }}" class="profile-image mb-2" alt="Profile Image"><br>
@endif
<input type="file" name="image" class="form-control">
@error('image') <small class="text-danger">{{ $message }}</small> @enderror
</div>
<div class="text-end">
<a href="{{ route('users.index') }}" class="btn btn-secondary">Back</a>
<button type="submit" class="btn btn-primary">Update</button>
</div>
</form>
<@endsection>
users/show.blade.php — Profile detai
// File: resources/views/users/show.blade.php
<@extends('layouts.app')>
<@section('title', 'User Profile Details')>
<@section('content')>
<div class="card shadow-sm p-4">
<div class="row">
<div class="col-md-3 text-center">
@if($profile->image)
<img src="{{ asset($profile->image) }}" alt="Profile Image" class="img-fluid rounded-circle mb-2" style="max-width: 150px;">
@else
<div class="text-muted">No Image</div>
@endif
</div>
<div class="col-md-9">
<h4>{{ $profile->first_name }} {{ $profile->last_name }}</h4>
<p><strong>Email:</strong> {{ $profile->email }}</p>
<p><strong>Created At:</strong> {{ $profile->created_at->format('d M Y, h:i A') }}</p>
<p><strong>Updated At:</strong> {{ $profile->updated_at->format('d M Y, h:i A') }}</p>
<div class="mt-3">
<a href="{{ route('users.index') }}" class="btn btn-secondary">Back</a>
<a href="{{ route('users.edit', $profile->id) }}" class="btn btn-warning">Edit</a>
<form action="{{ route('users.destroy', $profile->id) }}" method="POST" class="d-inline" onsubmit="return confirm('Delete this profile?')">
@csrf
@method('DELETE')
<button class="btn btn-danger">Delete</button>
</form>
</div>
</div>
</div>
</div>
<@endsection>
7. Image Upload Folder
Create this folder inside public/ and make it writable:
public/images/profiles
8. Launch the App
php artisan serve
Visit: http://localhost:8000/users
