Lessons → Laravel CRUD Tutorial

Laravel CRUD Tutorial

Laravel
⏱ 35 min read📖 IntermediateNot completed

CRUD (Create, Read, Update, Delete) is the foundation of almost every web application. In this tutorial you'll build a complete product management system from scratch in Laravel.

What We're Building

Overview
Product Management App
├── GET  /products          → List all products
├── GET  /products/create   → Show create form
├── POST /products          → Save new product
├── GET  /products/{id}     → Show single product
├── GET  /products/{id}/edit → Show edit form
├── PUT  /products/{id}     → Update product
└── DELETE /products/{id}  → Delete product

Step 1: Create Migration

Terminal
php artisan make:model Product -mcr
# -m = migration, -c = controller, -r = resource controller
database/migrations/create_products_table.php
<?php
public function up(): void
{
    Schema::create('products', function (Blueprint $table) {
        $table->id();
        $table->string('name');
        $table->text('description')->nullable();
        $table->decimal('price', 10, 2);
        $table->integer('stock')->default(0);
        $table->string('image')->nullable();
        $table->boolean('active')->default(true);
        $table->timestamps();
    });
}

// Run migration
// php artisan migrate

Step 2: Product Model

app/Models/Product.php
<?php
class Product extends Model
{
    protected $fillable = [
        'name', 'description', 'price', 'stock', 'image', 'active'
    ];

    protected $casts = [
        'price'  => 'decimal:2',
        'active' => 'boolean',
    ];

    // Formatted price accessor
    public function getFormattedPriceAttribute(): string {
        return '¥' . number_format($this->price, 0);
    }

    // Scope for active products
    public function scopeActive($query) {
        return $query->where('active', true);
    }
}
?>

Step 3: Resource Controller

app/Http/Controllers/ProductController.php
<?php
namespace App\Http\Controllers;

use App\Models\Product;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Storage;

class ProductController extends Controller
{
    // GET /products
    public function index(Request $request)
    {
        $products = Product::query()
            ->when($request->search, fn($q) =>
                $q->where('name', 'like', "%{$request->search}%"))
            ->when($request->active, fn($q) =>
                $q->where('active', $request->active === 'yes'))
            ->latest()
            ->paginate(10)
            ->withQueryString();

        return view('products.index', compact('products'));
    }

    // GET /products/create
    public function create()
    {
        return view('products.create');
    }

    // POST /products
    public function store(Request $request)
    {
        $validated = $request->validate([
            'name'        => 'required|string|max:255',
            'description' => 'nullable|string',
            'price'       => 'required|numeric|min:0',
            'stock'       => 'required|integer|min:0',
            'image'       => 'nullable|image|max:2048',
            'active'      => 'boolean',
        ]);

        if ($request->hasFile('image')) {
            $validated['image'] = $request->file('image')
                ->store('products', 'public');
        }

        Product::create($validated);

        return redirect()
            ->route('products.index')
            ->with('success', 'Product created successfully!');
    }

    // GET /products/{product}
    public function show(Product $product)
    {
        return view('products.show', compact('product'));
    }

    // GET /products/{product}/edit
    public function edit(Product $product)
    {
        return view('products.edit', compact('product'));
    }

    // PUT /products/{product}
    public function update(Request $request, Product $product)
    {
        $validated = $request->validate([
            'name'        => 'required|string|max:255',
            'description' => 'nullable|string',
            'price'       => 'required|numeric|min:0',
            'stock'       => 'required|integer|min:0',
            'image'       => 'nullable|image|max:2048',
            'active'      => 'boolean',
        ]);

        if ($request->hasFile('image')) {
            if ($product->image) {
                Storage::disk('public')->delete($product->image);
            }
            $validated['image'] = $request->file('image')
                ->store('products', 'public');
        }

        $product->update($validated);

        return redirect()
            ->route('products.index')
            ->with('success', 'Product updated successfully!');
    }

    // DELETE /products/{product}
    public function destroy(Product $product)
    {
        if ($product->image) {
            Storage::disk('public')->delete($product->image);
        }
        $product->delete();

        return redirect()
            ->route('products.index')
            ->with('success', 'Product deleted!');
    }
}
?>

Step 4: Routes

routes/web.php
<?php
use App\Http\Controllers\ProductController;

Route::resource('products', ProductController::class);

// This one line creates ALL 7 routes:
// GET    /products           products.index
// GET    /products/create    products.create
// POST   /products           products.store
// GET    /products/{id}      products.show
// GET    /products/{id}/edit products.edit
// PUT    /products/{id}      products.update
// DELETE /products/{id}      products.destroy
?>

Step 5: Views

resources/views/products/index.blade.php
@extends('layouts.app')
@section('title', 'Products')

@section('content')
<div class="max-w-6xl mx-auto p-6">

    <!-- Header -->
    <div class="flex justify-between items-center mb-6">
        <h1 class="text-2xl font-bold">Products ({{ $products->total() }})</h1>
        <a href="{{ route('products.create') }}"
           class="bg-blue-600 text-white px-4 py-2 rounded hover:bg-blue-700">
            + Add Product
        </a>
    </div>

    <!-- Search bar -->
    <form method="GET" class="mb-6 flex gap-2">
        <input type="text" name="search" value="{{ request('search') }}"
               placeholder="Search products..."
               class="flex-1 border rounded px-3 py-2" />
        <button type="submit" class="bg-gray-600 text-white px-4 py-2 rounded">Search</button>
    </form>

    @if(session('success'))
    <div class="bg-green-100 text-green-800 p-3 rounded mb-4">{{ session('success') }}</div>
    @endif

    <!-- Products Table -->
    <div class="bg-white rounded shadow overflow-hidden">
        <table class="w-full text-sm">
            <thead class="bg-gray-50">
                <tr>
                    <th class="p-4 text-left">Image</th>
                    <th class="p-4 text-left">Name</th>
                    <th class="p-4 text-left">Price</th>
                    <th class="p-4 text-left">Stock</th>
                    <th class="p-4 text-left">Status</th>
                    <th class="p-4 text-left">Actions</th>
                </tr>
            </thead>
            <tbody class="divide-y">
                @forelse($products as $product)
                <tr class="hover:bg-gray-50">
                    <td class="p-4">
                        @if($product->image)
                            <img src="{{ Storage::url($product->image) }}"
                                 class="w-12 h-12 object-cover rounded" />
                        @else
                            <div class="w-12 h-12 bg-gray-200 rounded flex items-center justify-center text-gray-400">?</div>
                        @endif
                    </td>
                    <td class="p-4 font-medium">{{ $product->name }}</td>
                    <td class="p-4">{{ $product->formatted_price }}</td>
                    <td class="p-4">
                        <span class="{{ $product->stock < 5 ? 'text-red-600 font-bold' : '' }}">
                            {{ $product->stock }}
                        </span>
                    </td>
                    <td class="p-4">
                        @if($product->active)
                            <span class="bg-green-100 text-green-700 px-2 py-1 rounded text-xs">Active</span>
                        @else
                            <span class="bg-red-100 text-red-700 px-2 py-1 rounded text-xs">Inactive</span>
                        @endif
                    </td>
                    <td class="p-4">
                        <div class="flex gap-3">
                            <a href="{{ route('products.edit', $product) }}" class="text-blue-600 hover:underline">Edit</a>
                            <form method="POST" action="{{ route('products.destroy', $product) }}"
                                  onsubmit="return confirm('Delete this product?')">
                                @csrf @method('DELETE')
                                <button class="text-red-600 hover:underline">Delete</button>
                            </form>
                        </div>
                    </td>
                </tr>
                @empty
                <tr><td colspan="6" class="p-8 text-center text-gray-400">No products found.</td></tr>
                @endforelse
            </tbody>
        </table>
    </div>
    <div class="mt-4">{{ $products->links() }}</div>
</div>
@endsection
💡
Route::resource() is magic! One line creates 7 routes with proper HTTP methods (GET, POST, PUT, DELETE). Always use it for standard CRUD — never manually write all 7 routes.
💡
Always use Route Model Binding! public function show(Product $product) — Laravel automatically finds the product by ID and throws a 404 if not found. No need to write Product::findOrFail($id) manually.
← Previous Lesson Next Lesson →
🧠

Test your knowledge!

Take a quiz to reinforce what you learned.

Take Quiz →