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.phpdatabase/migrations/xxxx_create_categories_table.phpapp/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:
| Method | URL |
|---|---|
| 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ế.




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