Kritim Yantra
Feb 28, 2025
Welcome to this step-by-step guide on creating a full CRUD (Create, Read, Update, Delete) operation in Laravel 12. In this tutorial, we’ll build a simple blog post system where you can manage blog posts, including uploading images. We’ll use Laravel’s powerful features to make this process efficient and straightforward. By the end of this post, you’ll have a solid understanding of how to implement CRUD operations in Laravel and handle image uploads.
CRUD stands for Create, Read, Update, and Delete. These are the four basic operations you can perform on data in a database. In our blog post system, this means:
Laravel is a popular PHP framework that makes building web applications easier with its clean syntax and powerful tools. Laravel 12 is the latest version (as of this writing), and we’ll use it for this tutorial.
First, we need to install Laravel 12 and create a new project. Open your terminal and run:
composer create-project laravel/laravel blog-app "12.*"
This command creates a new Laravel project called blog-app. Once it’s done, navigate to the project folder:
cd blog-app
Start the Laravel development server to check if everything works:
php artisan serve
Open your browser and visit http://localhost:8000
. You should see Laravel’s welcome page.
We’ll use MySQL for this tutorial. Create a database in MySQL (e.g., blog_db
) and configure Laravel to connect to it. Open the .env
file in your project’s root directory and update these lines:
DB_CONNECTION=mysql
DB_HOST=127.0.0.1
DB_PORT=3306
DB_DATABASE=blog_db
DB_USERNAME=your_username
DB_PASSWORD=your_password
Replace blog_db
, your_username
, and your_password
with your actual database name, username, and password.
We’ll create a Post
model to represent blog posts, along with a migration to set up the database table. Run this command:
php artisan make:model Post -m
The -m
flag tells Laravel to also create a migration file.
Open the migration file in database/migrations
(it’ll have a timestamp in its name, like 2023_10_10_000000_create_posts_table.php
). Update the up
method to define the posts table:
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
class CreatePostsTable extends Migration
{
public function up()
{
Schema::create('posts', function (Blueprint $table) {
$table->id();
$table->string('title');
$table->text('content');
$table->string('image')->nullable();
$table->timestamps();
});
}
public function down()
{
Schema::dropIfExists('posts');
}
}
This creates a table with:
Run the migration to create the table:
php artisan migrate
Routes tell Laravel how to handle web requests (like visiting a page or submitting a form). We’ll use a resource route to automatically set up all CRUD routes. Open routes/web.php
and add:
use App\Http\Controllers\PostController;
Route::resource('posts', PostController::class);
This single line creates seven routes for CRUD operations:
The controller handles the logic for each CRUD operation. Create a resource controller with this command:
php artisan make:controller PostController --resource
This generates PostController.php
in app/Http/Controllers
with empty methods for all CRUD operations. We’ll fill these in later.
Views are the HTML templates users see. Create a posts
folder in resources/views
(resources/views/posts
), and add these files:
index.blade.php
: List all posts.create.blade.php
: Form to create a post.edit.blade.php
: Form to edit a post.show.blade.php
: Display a single post.
We’ll also create a base layout file called app.blade.php
in resources/views/layouts
for shared HTML (like headers and styles).
app.blade.php
)<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Laravel CRUD</title>
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/css/bootstrap.min.css" rel="stylesheet">
</head>
<body>
<div class="container mt-5">
@yield('content')
</div>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/js/bootstrap.bundle.min.js"></script>
</body>
</html>
Controller: create and store Methods
use Illuminate\Http\Request;
use App\Models\Post;
use Illuminate\Support\Facades\Storage;
public function create()
{
return view('posts.create');
}
public function store(Request $request)
{
$request->validate([
'title' => 'required|string|max:255',
'content' => 'required',
'image' => 'nullable|image|max:2048', // Max 2MB
]);
$post = new Post();
$post->title = $request->title;
$post->content = $request->content;
if ($request->hasFile('image')) {
$imagePath = $request->file('image')->store('posts', 'public');
$post->image = $imagePath;
}
$post->save();
return redirect()->route('posts.index')->with('success', 'Post created successfully.');
}
create: Shows the form.
store: Validates input, saves the post, and stores the image if uploaded.
@extends('layouts.app')
@section('content')
<h1>Create Post</h1>
<form method="POST" action="{{ route('posts.store') }}" enctype="multipart/form-data">
@csrf
<div class="form-group">
<label for="title">Title</label>
<input type="text" name="title" class="form-control" required>
</div>
<div class="form-group">
<label for="content">Content</label>
<textarea name="content" class="form-control" required></textarea>
</div>
<div class="form-group">
<label for="image">Image</label>
<input type="file" name="image" class="form-control">
</div>
<button type="submit" class="btn btn-primary mt-3">Create</button>
</form>
@endsection
Here’s how the form looks: Create Post Form
Controller: index and show Methods
public function index()
{
$posts = Post::latest()->get();
return view('posts.index', compact('posts'));
}
public function show(Post $post)
{
return view('posts.show', compact('post'));
}
index: Fetches all posts and shows the list.
show: Displays a single post (Laravel automatically finds it by ID).
@extends('layouts.app')
@section('content')
<h1>All Posts</h1>
@if(session('success'))
<div class="alert alert-success">{{ session('success') }}</div>
@endif
<table class="table">
<thead>
<tr>
<th>Title</th>
<th>Image</th>
<th>Actions</th>
</tr>
</thead>
<tbody>
@foreach($posts as $post)
<tr>
<td>{{ $post->title }}</td>
<td>
@if($post->image)
<img src="{{ asset('storage/' . $post->image) }}" alt="{{ $post->title }}" width="100">
@else
No Image
@endif
</td>
<td>
<a href="{{ route('posts.show', $post->id) }}" class="btn btn-info">View</a>
<a href="{{ route('posts.edit', $post->id) }}" class="btn btn-warning">Edit</a>
<form action="{{ route('posts.destroy', $post->id) }}" method="POST" style="display:inline;">
@csrf
@method('DELETE')
<button type="submit" class="btn btn-danger">Delete</button>
</form>
</td>
</tr>
@endforeach
</tbody>
</table>
<a href="{{ route('posts.create') }}" class="btn btn-primary">Create New Post</a>
@endsection
Here’s the post list: Post List
Controller: edit and update Methods
public function edit(Post $post)
{
return view('posts.edit', compact('post'));
}
public function update(Request $request, Post $post)
{
$request->validate([
'title' => 'required|string|max:255',
'content' => 'required',
'image' => 'nullable|image|max:2048',
]);
$post->title = $request->title;
$post->content = $request->content;
if ($request->hasFile('image')) {
if ($post->image) {
Storage::delete('public/' . $post->image);
}
$imagePath = $request->file('image')->store('posts', 'public');
$post->image = $imagePath;
}
$post->save();
return redirect()->route('posts.index')->with('success', 'Post updated successfully.');
}
edit: Shows the edit form with the current post data.
update: Updates the post and replaces the image if a new one is uploaded.
@extends('layouts.app')
@section('content')
<h1>Edit Post</h1>
<form method="POST" action="{{ route('posts.update', $post->id) }}" enctype="multipart/form-data">
@csrf
@method('PUT')
<div class="form-group">
<label for="title">Title</label>
<input type="text" name="title" class="form-control" value="{{ $post->title }}" required>
</div>
<div class="form-group">
<label for="content">Content</label>
<textarea name="content" class="form-control" required>{{ $post->content }}</textarea>
</div>
<div class="form-group">
<label for="image">Image</label>
<input type="file" name="image" class="form-control">
@if($post->image)
<img src="{{ asset('storage/' . $post->image) }}" alt="{{ $post->title }}" width="100">
@endif
</div>
<button type="submit" class="btn btn-primary mt-3">Update</button>
</form>
@endsection
Here’s the edit form: Edit Post Form
Controller: destroy Method
public function destroy(Post $post)
{
if ($post->image) {
Storage::delete('public/' . $post->image);
}
$post->delete();
return redirect()->route('posts.index')->with('success', 'Post deleted successfully.');
}
This method deletes the post and its image from storage. The delete button is already in index.blade.php
as a form with @method('DELETE')
.
We’ve already handled image uploads in the store
and update
methods. To make images accessible, run:
php artisan storage:link
We’re already using Bootstrap in app.blade.php
. It makes our forms and tables look clean and responsive. No extra steps are needed here since Laravel includes it by default.
Start the server:
php artisan serve
Test each operation:
/posts/create
, fill the form, upload an image, and submit./posts
to see all posts, or click “View” to see one.If everything works, you’ll see success messages after each action.
Congratulations! You’ve built a full CRUD system in Laravel 12 with image uploads. You’ve learned how to:
This is a great starting point for building more complex Laravel applications. For better apps, always validate input and handle errors to keep things smooth.
Happy coding! Let me know in the comments if you have questions or want to see more tutorials like this!
No comments yet. Be the first to comment!
Please log in to post a comment:
Continue with Google