Laravel 12 CRUD Tutorial: Build a Complete Blog with Image Upload
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.
What is CRUD?
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:
- Create: Adding a new blog post.
- Read: Viewing all posts or a single post.
- Update: Editing an existing post.
- Delete: Removing a post.
What is Laravel?
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.
Prerequisites
- PHP 8.2 or higher
- Composer (a tool to manage PHP dependencies)
- A database like MySQL
- Basic knowledge of PHP and Laravel
Step 1: Setting up Laravel 12
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.
Step 2: Database Configuration
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.
Step 3: Creating a Model and Migration
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.
Define the Migration
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:
- id: A unique identifier for each post.
- title: The post’s title.
- content: The post’s body.
- image: The path to an uploaded image (optional).
- timestamps: Created and updated timestamps.
Run the migration to create the table:
php artisan migrate
Step 4: Creating Routes
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:
- GET /posts (index): List all posts.
- GET /posts/create (create): Show the create form.
- POST /posts (store): Save a new post.
- GET /posts/{id} (show): Show one post.
- GET /posts/{id}/edit (edit): Show the edit form.
- PUT /posts/{id} (update): Update a post.
- DELETE /posts/{id} (destroy): Delete a post.
Step 5: Creating a Controller
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.
Step 6: Creating Views
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).
Base Layout (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>
Step 7: Implementing CRUD Operations
7.1 Create Operation
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.
View: create.blade.php
@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
7.2 Read Operation
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).
View: index.blade.php
@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
7.3 Update Operation
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.
View: edit.blade.php
@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
7.4 Delete Operation
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').
Step 8: Adding Image Upload
We’ve already handled image uploads in the store and update methods. To make images accessible, run:
php artisan storage:link
Step 9: Styling with Bootstrap
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.
Step 10: Testing the Application
Start the server:
php artisan serve
Test each operation:
- Create: Visit
/posts/create, fill the form, upload an image, and submit. - Read: Go to
/poststo see all posts, or click “View” to see one. - Update: Click “Edit”, change details, upload a new image, and save.
- Delete: Click “Delete” and confirm.
If everything works, you’ll see success messages after each action.
Conclusion
Congratulations! You’ve built a full CRUD system in Laravel 12 with image uploads. You’ve learned how to:
- Set up a Laravel project.
- Configure a database.
- Create models, migrations, routes, controllers, and views.
- Handle image uploads and display them.
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!
Projects to explore next
Source-code projects from the KritiMyantra catalogue. Check each project's details before buying.
Flask Authentication System (Free Version) - Kritim Yantra
🛠 Features✔ Google & GitHub Authentication✔ User Registration & Login✔ Profile/Dashboard Access✔ Secure & Mi...
Flask Blog CMS with Admin Dashboard — Python Source Code
A Flask-based blogging website with a built-in admin dashboard. Manage articles, images, categories, tags and featured p...
Laravel 12 + ReactJS Starter Kit: Add Multi-Language Support (Arabic, English, Spanish) with Blog CRUD
🚀 Installation & Setup Download & Extract Get the ZIP from kritimyantra.com and extract it. Includes preconfigu...
Comments
No comments yet. Be the first to comment.
Sign in to join the discussion.