Laravel 12 CRUD Tutorial: Build a Complete Blog with Image Upload

Author

Kritim Yantra

Feb 28, 2025

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 /posts to 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!

Tags

Laravel Php

Comments

No comments yet. Be the first to comment!

Please log in to post a comment:

Continue with Google

Related Posts

Understanding SOLID Design Principles in Laravel (For Beginners)
Kritim Yantra Kritim Yantra
Feb 24, 2025
Laravel 12 Roles and Permissions Setup: Complete Guide
Kritim Yantra Kritim Yantra
Feb 28, 2025