NTM Solutions

Thứ Tư, 5 tháng 8, 2026

🚀 Laravel 12 (2026) — Bài 26 — CRUD Categories trong Laravel 12

Từ bài này chúng ta sẽ bắt đầu xây dựng dự án Blog CMS hoàn chỉnh. 

Chức năng đầu tiên là quản lý danh mục bài viết (Categories) với đầy đủ các thao tác thêm – sửa – xóa – tìm kiếm – phân trang.

Mục tiêu bài học

  • Tạo Resource Controller cho Category.

  • Xây dựng giao diện quản trị bằng Blade + Tailwind CSS.

  • Validate dữ liệu đầu vào.

  • Tìm kiếm theo tên danh mục.

  • Phân trang dữ liệu.

  • Hoàn thiện module CRUD chuẩn Laravel 12.


1. Tạo Model, Migration và Controller

Nếu chưa có Category, tạo nhanh bằng Artisan:

Bash

php artisan make:model Category -mcr

Laravel sẽ tạo:

  • app/Models/Category.php

  • database/migrations/xxxx_create_categories_table.php

  • app/Http/Controllers/CategoryController.php

Migration

PHP

Schema::create('categories', function (Blueprint $table) {
    $table->id();
    $table->string('name');
    $table->string('slug')->unique();
    $table->text('description')->nullable();
    $table->timestamps();
});

Chạy migration:

Bash

php artisan migrate

2. Khai báo Model

PHP

namespace App\Models;

use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Factories\HasFactory;

class Category extends Model
{
    use HasFactory;

    protected $fillable = [
        'name',
        'slug',
        'description'
    ];
}

3. Định nghĩa Resource Route

Mở routes/web.php:

PHP

use App\Http\Controllers\CategoryController;

Route::middleware(['auth'])->group(function () {
    Route::resource('categories', CategoryController::class);
});

Laravel tự tạo 7 route chuẩn:

MethodURL
GET/categories
GET/categories/create
POST/categories
GET/categories/{"{category}"}
GET/categories/{"{category}"}/edit
PUT/categories/{"{category}"}
DELETE/categories/{"{category}"}

4. Hiển thị danh sách Categories

Controller

PHP

use App\Models\Category;
use Illuminate\Http\Request;

public function index(Request $request)
{
    $categories = Category::query()
        ->when($request->keyword, function ($query, $keyword) {
            $query->where('name', 'like', "%{$keyword}%");
        })
        ->latest()
        ->paginate(10)
        ->withQueryString();

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

View: resources/views/categories/index.blade.php

PHP

<x-app-layout>
    <div class="max-w-7xl mx-auto p-6">

        <div class="flex justify-between items-center mb-6">
            <h1 class="text-2xl font-bold">Quản lý Categories</h1>

            <a href="{{ route('categories.create') }}"
               class="bg-blue-600 text-white px-4 py-2 rounded-lg hover:bg-blue-700">
                + Thêm Category
            </a>
        </div>

        <form method="GET" class="mb-4">
            <input type="text"
                   name="keyword"
                   value="{{ request('keyword') }}"
                   placeholder="Tìm kiếm category..."
                   class="border rounded-lg px-4 py-2 w-80">

            <button class="bg-gray-800 text-white px-4 py-2 rounded-lg">
                Tìm
            </button>
        </form>

        <div class="bg-white rounded-xl shadow overflow-hidden">
            <table class="min-w-full">
                <thead class="bg-gray-100">
                    <tr>
                        <th class="px-4 py-3 text-left">ID</th>
                        <th class="px-4 py-3 text-left">Tên</th>
                        <th class="px-4 py-3 text-left">Slug</th>
                        <th class="px-4 py-3 text-left">Ngày tạo</th>
                        <th class="px-4 py-3 text-right">Thao tác</th>
                    </tr>
                </thead>

                <tbody class="divide-y">
                    @forelse($categories as $category)
                        <tr class="hover:bg-gray-50">
                            <td class="px-4 py-3">{{ $category->id }}</td>
                            <td class="px-4 py-3 font-medium">
                                {{ $category->name }}
                            </td>
                            <td class="px-4 py-3 text-gray-600">
                                {{ $category->slug }}
                            </td>
                            <td class="px-4 py-3">
                                {{ $category->created_at->format('d/m/Y') }}
                            </td>
                            <td class="px-4 py-3 text-right space-x-2">
                                <a href="{{ route('categories.edit', $category) }}"
                                   class="text-blue-600 hover:underline">
                                    Sửa
                                </a>

                                <form action="{{ route('categories.destroy', $category) }}"
                                      method="POST"
                                      class="inline">
                                    @csrf
                                    @method('DELETE')

                                    <button
                                        onclick="return confirm('Xóa category này?')"
                                        class="text-red-600 hover:underline">
                                        Xóa
                                    </button>
                                </form>
                            </td>
                        </tr>
                    @empty
                        <tr>
                            <td colspan="5" class="text-center py-8 text-gray-500">
                                Chưa có dữ liệu
                            </td>
                        </tr>
                    @endforelse
                </tbody>
            </table>
        </div>

        <div class="mt-6">
            {{ $categories->links() }}
        </div>

    </div>
</x-app-layout>

5. Tạo Category mới

Form Create

PHP

<form action="{{ route('categories.store') }}" method="POST">
    @csrf

    <div class="mb-4">
        <label class="block mb-1">Tên Category</label>
        <input type="text"
               name="name"
               value="{{ old('name') }}"
               class="w-full border rounded-lg px-4 py-2">
        @error('name')
            <p class="text-red-500 text-sm mt-1">{{ $message }}</p>
        @enderror
    </div>

    <div class="mb-4">
        <label class="block mb-1">Slug</label>
        <input type="text"
               name="slug"
               value="{{ old('slug') }}"
               class="w-full border rounded-lg px-4 py-2">
    </div>

    <div class="mb-4">
        <label class="block mb-1">Mô tả</label>
        <textarea name="description"
                  class="w-full border rounded-lg px-4 py-2"
                  rows="4">{{ old('description') }}</textarea>
    </div>

    <button class="bg-blue-600 text-white px-6 py-2 rounded-lg">
        Lưu
    </button>
</form>


Controller Store

PHP

public function store(Request $request)
{
    $validated = $request->validate([
        'name' => 'required|min:2|max:100',
        'slug' => 'required|unique:categories,slug',
        'description' => 'nullable|max:1000',
    ]);

    Category::create($validated);

    return redirect()
        ->route('categories.index')
        ->with('success', 'Thêm category thành công!');
}

6. Cập nhật Category

Controller Edit

PHP

public function edit(Category $category)
{
    return view('categories.edit', compact('category'));
}

Controller Update

PHP

public function update(Request $request, Category $category)
{
    $validated = $request->validate([
        'name' => 'required|min:2|max:100',
        'slug' => 'required|unique:categories,slug,' . $category->id,
        'description' => 'nullable|max:1000',
    ]);

    $category->update($validated);

    return redirect()
        ->route('categories.index')
        ->with('success', 'Cập nhật thành công!');
}

Điểm quan trọng là rule:

PHP

'slug' => 'required|unique:categories,slug,' . $category->id

Điều này cho phép giữ nguyên slug hiện tại mà không bị báo lỗi trùng.

7. Xóa Category

PHP

public function destroy(Category $category)
{
    $category->delete();

    return redirect()
        ->route('categories.index')
        ->with('success', 'Đã xóa category!');
}

Trong bài 30 chúng ta sẽ nâng cấp thành Soft Delete.

8. Tìm kiếm Categories

Chức năng tìm kiếm đã được tích hợp trong phương thức index():

PHP

->when($request->keyword, function ($query, $keyword) {
    $query->where('name', 'like', "%{$keyword}%");
})

Ví dụ:

  • Nhập laravel → hiển thị các category chứa từ “laravel”.

  • Nhập php → hiển thị các category liên quan PHP.

  • Để trống → hiển thị toàn bộ dữ liệu.

Nhờ withQueryString(), khi chuyển trang Laravel vẫn giữ lại từ khóa tìm kiếm.

9. Phân trang

PHP

->paginate(10)

Hiển thị:

PHP

{{ $categories->links() }}

Laravel 12 mặc định sử dụng giao diện phân trang tương thích Tailwind CSS.

10. Hiển thị thông báo thành công

Thêm vào đầu trang:

PHP

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

Kết quả:

  • Thêm mới → “Thêm category thành công!”

  • Cập nhật → “Cập nhật thành công!”

  • Xóa → “Đã xóa category!”

11. Cấu trúc thư mục sau khi hoàn thành

app/
├── Http/
│   └── Controllers/
│       └── CategoryController.php
├── Models/
│   └── Category.php

resources/
└── views/
    └── categories/
        ├── index.blade.php
        ├── create.blade.php
        └── edit.blade.php

routes/
└── web.php

12. Kết quả đạt được

Sau bài học này, module Quản lý Categories của Blog CMS đã có đầy đủ:

CRUD

Quản lý dữ liệu

  • Danh sách Categories

  • Thêm mới Category

  • Cập nhật Category

  • Xóa Category

Validation

Kiểm tra dữ liệu

  • Validate tên danh mục

  • Validate slug duy nhất

  • Hiển thị lỗi ngay trên form

Search

Tìm kiếm

  • Lọc theo tên category

  • Giữ từ khóa khi chuyển trang

Pagination

Phân trang

  • 10 bản ghi mỗi trang

  • Giao diện Tailwind mặc định của Laravel 12

Đây chính là mẫu CRUD chuẩn mà chúng ta sẽ tái sử dụng cho nhiều module khác trong dự án.

Bài tiếp theo

Sang Bài 27 — CRUD Posts, chúng ta sẽ kết hợp:

  • Relationship giữa Post và Category.

  • Gán bài viết cho User.

  • Upload hình ảnh.

  • Validate dữ liệu phức tạp hơn.

  • Hiển thị danh sách bài viết kèm tên danh mục và tác giả.

Từ bài 27 trở đi, dự án Blog CMS sẽ bắt đầu có cấu trúc giống một hệ thống quản trị nội dung thực tế.

03 - Categories Table with Livewire | Laravel Daily

Không có nhận xét nào:

Đăng nhận xét

Facebook Youtube RSS