lt;pgt;Từ bài này chúng ta sẽ bắt đầu xây dựng dự án Blog CMS hoàn chỉnh.lt;/pgt;lt;pgt;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.lt;/pgt;lt;h3gt;Mục tiêu bài họclt;/h3gt;lt;ulgt;lt;ligt;lt;pgt;Tạo Resource Controller cho Category.lt;/pgt;lt;/ligt;lt;ligt;lt;pgt;Xây dựng giao diện quản trị bằng Blade + Tailwind CSS.lt;/pgt;lt;/ligt;lt;ligt;lt;pgt;Validate dữ liệu đầu vào.lt;/pgt;lt;/ligt;lt;ligt;lt;pgt;Tìm kiếm theo tên danh mục.lt;/pgt;lt;/ligt;lt;ligt;lt;pgt;Phân trang dữ liệu.lt;/pgt;lt;/ligt;lt;ligt;lt;pgt;Hoàn thiện module CRUD chuẩn Laravel 12.lt;/pgt;lt;div class=quot;separatorquot; style=quot;clear: both; text-align: center;quot;gt;lt;a href=quot;https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEjQdciyuCHE742MjSvJDNRyqhBP5y58AK2pMxWn7k1TCg15lBHhKA5JGrqkwed8u2vx-yDXyBgsT8XlXMTnDfbDuZIUGBnAQ94KkjFk4mdaEyGJK2qjHrKhsfw_8BpxmtBiQPoGI9aGuw5hjmKyvV94Y6xysNbMF2GkyZWwk_VM3XiTTcyi5thKbDkhsdc/s1536/laravel-thu.pngquot; style=quot;margin-left: 1em; margin-right: 1em;quot;gt;lt;img border=quot;0quot; data-original-height=quot;1024quot; data-original-width=quot;1536quot; height=quot;213quot; src=quot;https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEjQdciyuCHE742MjSvJDNRyqhBP5y58AK2pMxWn7k1TCg15lBHhKA5JGrqkwed8u2vx-yDXyBgsT8XlXMTnDfbDuZIUGBnAQ94KkjFk4mdaEyGJK2qjHrKhsfw_8BpxmtBiQPoGI9aGuw5hjmKyvV94Y6xysNbMF2GkyZWwk_VM3XiTTcyi5thKbDkhsdc/s320/laravel-thu.pngquot; width=quot;320quot; /gt;lt;/agt;lt;spangt;lt;a name=#39;more#39;gt;lt;/agt;lt;/spangt;lt;/divgt;lt;br /gt;lt;pgt;lt;/pgt;lt;/ligt;lt;/ulgt;lt;h3gt;1. Tạo Model, Migration và Controllerlt;/h3gt;lt;pgt;Trường hợp 01: các bài trước đã tạo category rồi nhưng thiếu columnlt;/pgt;lt;h3 class=quot;PDq2pG_selectionAnchorContainerquot; data-end=quot;199quot; data-section-id=quot;9rzhbcquot; data-start=quot;143quot;gt;Tạo migration mới để thêm cộtlt;span aria-hidden=quot;truequot; class=quot;PDq2pG_selectionAnchorquot;gt;lt;/spangt;lt;/h3gt;lt;pre class=quot;overflow-visible! px-0!quot; data-end=quot;303quot; data-start=quot;201quot;gt;lt;div class=quot;relative w-full mt-4 mb-1quot;gt;lt;div class=quot;quot;gt;lt;div class=quot;contentsquot;gt;lt;div class=quot;border border-token-border-light border-radius-3xl corner-superellipse/1.1 rounded-3xlquot;gt;lt;div class=quot;relative h-full w-full border-radius-3xl bg-(--code-block-surface) corner-superellipse/1.1 overflow-clip rounded-3xl [--code-block-surface:var(--bg-elevated-secondary)] dark:[--code-block-surface:var(--composer-surface-primary)] lxnfua_clipPathFallbackquot;gt;lt;div class=quot;pointer-events-none absolute inset-x-4 top-12 bottom-4quot;gt;lt;div class=quot;pointer-events-none sticky z-40 shrink-0 z-1!quot;gt;lt;div class=quot;sticky bg-token-border-lightquot;gt;lt;/divgt;lt;/divgt;lt;/divgt;lt;div class=quot;relativequot;gt;lt;div class=quot;h-full min-h-0 min-w-0quot;gt;lt;div class=quot;h-full min-h-0 min-w-0quot;gt;lt;div class=quot;quot;gt;lt;div class=quot;relativequot;gt;lt;div class=quot;quot;gt;lt;div class=quot;relative z-0 flex max-w-fullquot;gt;lt;div class=quot;q9tKkq_viewer cm-editor z-10 light:cm-light dark:cm-light flex h-full w-full flex-col items-stretch ͼs ͼ16quot; dir=quot;ltrquot; id=quot;code-block-viewerquot;gt;lt;div class=quot;cm-scrollerquot;gt;lt;pre class=quot;cm-content q9tKkq_readonly m-0quot;gt;lt;codegt;php artisan lt;span class=quot;ͼ10quot;gt;makelt;/spangt;:migration add_slug_and_description_to_categories_table lt;span class=quot;ͼ12quot;gt;--tablelt;/spangt;lt;span class=quot;ͼvquot;gt;=lt;/spangt;categorieslt;/codegt;lt;/pregt;lt;/divgt;lt;/divgt;lt;/divgt;lt;/divgt;lt;/divgt;lt;/divgt;lt;/divgt;lt;/divgt;lt;div class=quot;quot;gt;lt;div class=quot;quot;gt;lt;/divgt;lt;/divgt;lt;/divgt;lt;/divgt;lt;/divgt;lt;/divgt;lt;/divgt;lt;/divgt;lt;/pregt;lt;p data-end=quot;326quot; data-start=quot;305quot;gt;Trong file migration:lt;/pgt;lt;pre class=quot;overflow-visible! px-0!quot; data-end=quot;891quot; data-start=quot;328quot;gt;lt;div class=quot;relative w-full mt-4 mb-1quot;gt;lt;div class=quot;quot;gt;lt;div class=quot;contentsquot;gt;lt;div class=quot;border border-token-border-light border-radius-3xl corner-superellipse/1.1 rounded-3xlquot;gt;lt;div class=quot;relative h-full w-full border-radius-3xl bg-(--code-block-surface) corner-superellipse/1.1 overflow-clip rounded-3xl [--code-block-surface:var(--bg-elevated-secondary)] dark:[--code-block-surface:var(--composer-surface-primary)] lxnfua_clipPathFallbackquot;gt;lt;div class=quot;pointer-events-none absolute inset-x-4 top-12 bottom-4quot;gt;lt;div class=quot;pointer-events-none sticky z-40 shrink-0 z-1!quot;gt;lt;div class=quot;sticky bg-token-border-lightquot;gt;lt;/divgt;lt;/divgt;lt;/divgt;lt;div class=quot;relativequot;gt;lt;div class=quot;h-full min-h-0 min-w-0quot;gt;lt;div class=quot;h-full min-h-0 min-w-0quot;gt;lt;div class=quot;quot;gt;lt;div class=quot;relativequot;gt;lt;div class=quot;quot;gt;lt;div class=quot;relative z-0 flex max-w-fullquot;gt;lt;div class=quot;q9tKkq_viewer cm-editor z-10 light:cm-light dark:cm-light flex h-full w-full flex-col items-stretch ͼs ͼ16quot; dir=quot;ltrquot; id=quot;code-block-viewerquot;gt;lt;div class=quot;cm-scrollerquot;gt;lt;pre class=quot;cm-content q9tKkq_readonly m-0quot;gt;lt;codegt;use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
public function up(): void
{
Schema::table(#39;categories#39;, function (Blueprint $table) {
$table-gt;string(#39;slug#39;)-gt;unique()-gt;after(#39;name#39;);
$table-gt;text(#39;description#39;)-gt;nullable()-gt;after(#39;slug#39;);
});
}
public function down(): void
{
Schema::table(#39;categories#39;, function (Blueprint $table) {
$table-gt;dropColumn([#39;slug#39;, #39;description#39;]);
});
}
};lt;/codegt;lt;/pregt;lt;/divgt;lt;/divgt;lt;/divgt;lt;/divgt;lt;/divgt;lt;/divgt;lt;/divgt;lt;/divgt;lt;/divgt;lt;/divgt;lt;/divgt;lt;/divgt;lt;/divgt;lt;/divgt;lt;/pregt;lt;pre class=quot;overflow-visible! px-0!quot; data-end=quot;1087quot; data-start=quot;1050quot;gt;lt;div class=quot;relative w-full mt-4 mb-1quot;gt;lt;div class=quot;quot;gt;lt;div class=quot;contentsquot;gt;lt;div class=quot;border border-token-border-light border-radius-3xl corner-superellipse/1.1 rounded-3xlquot;gt;lt;div class=quot;relative h-full w-full border-radius-3xl bg-(--code-block-surface) corner-superellipse/1.1 overflow-clip rounded-3xl [--code-block-surface:var(--bg-elevated-secondary)] dark:[--code-block-surface:var(--composer-surface-primary)] lxnfua_clipPathFallbackquot;gt;lt;div class=quot;relativequot;gt;lt;div class=quot;quot;gt;lt;div class=quot;quot;gt;lt;/divgt;lt;/divgt;lt;/divgt;lt;/divgt;lt;/divgt;lt;/divgt;lt;/divgt;lt;/divgt;lt;/pregt;lt;pgt;
lt;/pgt;lt;h2 class=quot;PDq2pG_selectionAnchorContainerquot; data-end=quot;366quot; data-section-id=quot;1yp4vazquot; data-start=quot;325quot;gt;Sửa CategoryFactory (nếu dùng factory)lt;span aria-hidden=quot;truequot; class=quot;PDq2pG_selectionAnchorquot;gt;lt;/spangt;lt;/h2gt;lt;p data-end=quot;371quot; data-start=quot;368quot;gt;Mở:lt;/pgt;lt;pre class=quot;overflow-visible! px-0!quot; data-end=quot;419quot; data-start=quot;373quot;gt;lt;div class=quot;relative w-full mt-4 mb-1quot;gt;lt;div class=quot;quot;gt;lt;div class=quot;contentsquot;gt;lt;div class=quot;relativequot;gt;lt;div class=quot;h-full min-h-0 min-w-0quot;gt;lt;div class=quot;h-full min-h-0 min-w-0quot;gt;lt;div class=quot;border border-token-border-light border-radius-3xl corner-superellipse/1.1 rounded-3xlquot;gt;lt;div class=quot;h-full w-full border-radius-3xl bg-(--code-block-surface) corner-superellipse/1.1 overflow-clip rounded-3xl [--code-block-surface:var(--bg-elevated-secondary)] dark:[--code-block-surface:var(--composer-surface-primary)] lxnfua_clipPathFallbackquot;gt;lt;div class=quot;pointer-events-none absolute end-1.5 top-1 z-2 md:end-2 md:top-1quot;gt;lt;/divgt;lt;div class=quot;relativequot;gt;lt;div class=quot;pe-11 pt-3quot;gt;lt;div class=quot;relative z-0 flex max-w-fullquot;gt;lt;div class=quot;q9tKkq_viewer cm-editor z-10 light:cm-light dark:cm-light flex h-full w-full flex-col items-stretch ͼs ͼ16quot; dir=quot;ltrquot; id=quot;code-block-viewerquot;gt;lt;div class=quot;cm-scrollerquot;gt;lt;pre class=quot;cm-content q9tKkq_readonly m-0quot;gt;lt;codegt;database/factories/CategoryFactory.phplt;/codegt;lt;/pregt;lt;/divgt;lt;/divgt;lt;/divgt;lt;/divgt;lt;/divgt;lt;/divgt;lt;/divgt;lt;/divgt;lt;/divgt;lt;div class=quot;quot;gt;lt;div class=quot;quot;gt;lt;/divgt;lt;/divgt;lt;/divgt;lt;/divgt;lt;/divgt;lt;/divgt;lt;/pregt;lt;p data-end=quot;433quot; data-start=quot;421quot;gt;Nếu đang là:lt;/pgt;lt;pre class=quot;overflow-visible! px-0!quot; data-end=quot;495quot; data-start=quot;435quot;gt;lt;div class=quot;relative w-full mt-4 mb-1quot;gt;lt;div class=quot;quot;gt;lt;div class=quot;contentsquot;gt;lt;div class=quot;border border-token-border-light border-radius-3xl corner-superellipse/1.1 rounded-3xlquot;gt;lt;div class=quot;relative h-full w-full border-radius-3xl bg-(--code-block-surface) corner-superellipse/1.1 overflow-clip rounded-3xl [--code-block-surface:var(--bg-elevated-secondary)] dark:[--code-block-surface:var(--composer-surface-primary)] lxnfua_clipPathFallbackquot;gt;lt;div class=quot;pointer-events-none absolute inset-x-4 top-12 bottom-4quot;gt;lt;div class=quot;pointer-events-none sticky z-40 shrink-0 z-1!quot;gt;lt;div class=quot;sticky bg-token-border-lightquot;gt;lt;/divgt;lt;/divgt;lt;/divgt;lt;div class=quot;relativequot;gt;lt;div class=quot;h-full min-h-0 min-w-0quot;gt;lt;div class=quot;h-full min-h-0 min-w-0quot;gt;lt;div class=quot;quot;gt;lt;div class=quot;relativequot;gt;lt;div class=quot;quot;gt;lt;div class=quot;relative z-0 flex max-w-fullquot;gt;lt;div class=quot;q9tKkq_viewer cm-editor z-10 light:cm-light dark:cm-light flex h-full w-full flex-col items-stretch ͼs ͼ16quot; dir=quot;ltrquot; id=quot;code-block-viewerquot;gt;lt;div class=quot;cm-scrollerquot;gt;lt;pre class=quot;cm-content q9tKkq_readonly m-0quot;gt;lt;codegt;return [
#39;name#39; =gt; fake()-gt;words(2, true),
];lt;/codegt;lt;/pregt;lt;/divgt;lt;/divgt;lt;/divgt;lt;/divgt;lt;/divgt;lt;/divgt;lt;/divgt;lt;/divgt;lt;div class=quot;quot;gt;lt;div class=quot;quot;gt;lt;/divgt;lt;/divgt;lt;/divgt;lt;/divgt;lt;/divgt;lt;/divgt;lt;/divgt;lt;/divgt;lt;/pregt;lt;p data-end=quot;507quot; data-start=quot;497quot;gt;đổi thành:lt;/pgt;lt;pre class=quot;overflow-visible! px-0!quot; data-end=quot;689quot; data-start=quot;509quot;gt;lt;div class=quot;relative w-full mt-4 mb-1quot;gt;lt;div class=quot;quot;gt;lt;div class=quot;contentsquot;gt;lt;div class=quot;border border-token-border-light border-radius-3xl corner-superellipse/1.1 rounded-3xlquot;gt;lt;div class=quot;relative h-full w-full border-radius-3xl bg-(--code-block-surface) corner-superellipse/1.1 overflow-clip rounded-3xl [--code-block-surface:var(--bg-elevated-secondary)] dark:[--code-block-surface:var(--composer-surface-primary)] lxnfua_clipPathFallbackquot;gt;lt;div class=quot;pointer-events-none absolute inset-x-4 top-12 bottom-4quot;gt;lt;div class=quot;pointer-events-none sticky z-40 shrink-0 z-1!quot;gt;lt;div class=quot;sticky bg-token-border-lightquot;gt;lt;/divgt;lt;/divgt;lt;/divgt;lt;div class=quot;relativequot;gt;lt;div class=quot;h-full min-h-0 min-w-0quot;gt;lt;div class=quot;h-full min-h-0 min-w-0quot;gt;lt;div class=quot;quot;gt;lt;div class=quot;relativequot;gt;lt;div class=quot;quot;gt;lt;div class=quot;relative z-0 flex max-w-fullquot;gt;lt;div class=quot;q9tKkq_viewer cm-editor z-10 light:cm-light dark:cm-light flex h-full w-full flex-col items-stretch ͼs ͼ16quot; dir=quot;ltrquot; id=quot;code-block-viewerquot;gt;lt;div class=quot;cm-scrollerquot;gt;lt;pre class=quot;cm-content q9tKkq_readonly m-0quot;gt;lt;codegt;use Illuminate\Support\Str;
return [
#39;name#39; =gt; $name = fake()-gt;unique()-gt;words(2, true),
#39;slug#39; =gt; Str::slug($name),
#39;description#39; =gt; fake()-gt;sentence(),
];lt;/codegt;lt;/pregt;lt;/divgt;lt;/divgt;lt;/divgt;lt;/divgt;lt;/divgt;lt;/divgt;lt;/divgt;lt;/divgt;lt;div class=quot;quot;gt;lt;div class=quot;quot;gt;lt;/divgt;lt;/divgt;lt;/divgt;lt;/divgt;lt;/divgt;lt;/divgt;lt;/divgt;lt;/divgt;lt;/pregt;lt;hr data-end=quot;694quot; data-start=quot;691quot; /gt;lt;p data-end=quot;1112quot; data-start=quot;1089quot;gt;
lt;/pgt;lt;p class=quot;PDq2pG_selectionAnchorContainerquot; data-end=quot;1088quot; data-start=quot;1070quot;gt;lt;bgt;File seeder:lt;/bgt;lt;span aria-hidden=quot;truequot; class=quot;PDq2pG_selectionAnchorquot;gt;lt;/spangt;lt;/pgt;
lt;pre class=quot;overflow-visible! px-0!quot; data-end=quot;1402quot; data-start=quot;1090quot;gt;lt;div class=quot;relative w-full mt-4 mb-1quot;gt;lt;div class=quot;quot;gt;lt;div class=quot;contentsquot;gt;lt;div class=quot;border border-token-border-light border-radius-3xl corner-superellipse/1.1 rounded-3xlquot;gt;lt;div class=quot;relative h-full w-full border-radius-3xl bg-(--code-block-surface) corner-superellipse/1.1 overflow-clip rounded-3xl [--code-block-surface:var(--bg-elevated-secondary)] dark:[--code-block-surface:var(--composer-surface-primary)] lxnfua_clipPathFallbackquot;gt;lt;div class=quot;pointer-events-none absolute inset-x-4 top-12 bottom-4quot;gt;lt;div class=quot;pointer-events-none sticky z-40 shrink-0 z-1!quot;gt;lt;div class=quot;sticky bg-token-border-lightquot;gt;lt;/divgt;lt;/divgt;lt;/divgt;lt;div class=quot;relativequot;gt;lt;div class=quot;h-full min-h-0 min-w-0quot;gt;lt;div class=quot;h-full min-h-0 min-w-0quot;gt;lt;div class=quot;quot;gt;lt;div class=quot;relativequot;gt;lt;div class=quot;quot;gt;lt;div class=quot;relative z-0 flex max-w-fullquot;gt;lt;div class=quot;q9tKkq_viewer cm-editor z-10 light:cm-light dark:cm-light flex h-full w-full flex-col items-stretch ͼs ͼ16quot; dir=quot;ltrquot; id=quot;code-block-viewerquot;gt;lt;div class=quot;cm-scrollerquot;gt;lt;pre class=quot;cm-content q9tKkq_readonly m-0quot;gt;lt;codegt;use App\Models\Category;
use Illuminate\Support\Str;
public function run(): void
{
foreach ([#39;Laravel#39;, #39;PHP#39;, #39;MySQL#39;] as $name) {
Category::create([
#39;name#39; =gt; $name,
#39;slug#39; =gt; Str::slug($name),
#39;description#39; =gt; quot;Danh mục {$name}quot;,
]);
}
}lt;/codegt;lt;/pregt;lt;/divgt;lt;/divgt;lt;/divgt;lt;/divgt;lt;/divgt;lt;/divgt;lt;/divgt;lt;/divgt;lt;div class=quot;quot;gt;lt;div class=quot;quot;gt;lt;/divgt;lt;/divgt;lt;/divgt;lt;/divgt;lt;/divgt;lt;/divgt;lt;/divgt;lt;/divgt;lt;/pregt;
lt;h2 data-end=quot;1436quot; data-section-id=quot;1wi924lquot; data-start=quot;1404quot;gt;Sau đólt;/h2gt;
lt;pre class=quot;overflow-visible! px-0!quot; data-end=quot;1575quot; data-start=quot;1531quot;gt;lt;div class=quot;relative w-full mt-4 mb-1quot;gt;lt;div class=quot;quot;gt;lt;div class=quot;contentsquot;gt;lt;div class=quot;border border-token-border-light border-radius-3xl corner-superellipse/1.1 rounded-3xlquot;gt;lt;div class=quot;relative h-full w-full border-radius-3xl bg-(--code-block-surface) corner-superellipse/1.1 overflow-clip rounded-3xl [--code-block-surface:var(--bg-elevated-secondary)] dark:[--code-block-surface:var(--composer-surface-primary)] lxnfua_clipPathFallbackquot;gt;lt;div class=quot;pointer-events-none absolute inset-x-4 top-12 bottom-4quot;gt;lt;div class=quot;pointer-events-none sticky z-40 shrink-0 z-1!quot;gt;lt;div class=quot;sticky bg-token-border-lightquot;gt;lt;/divgt;lt;/divgt;lt;/divgt;lt;div class=quot;relativequot;gt;lt;div class=quot;h-full min-h-0 min-w-0quot;gt;lt;div class=quot;h-full min-h-0 min-w-0quot;gt;lt;div class=quot;quot;gt;lt;div class=quot;relativequot;gt;lt;div class=quot;quot;gt;lt;div class=quot;relative z-0 flex max-w-fullquot;gt;lt;div class=quot;q9tKkq_viewer cm-editor z-10 light:cm-light dark:cm-light flex h-full w-full flex-col items-stretch ͼs ͼ16quot; dir=quot;ltrquot; id=quot;code-block-viewerquot;gt;lt;div class=quot;cm-scrollerquot;gt;lt;pre class=quot;cm-content q9tKkq_readonly m-0quot;gt;lt;codegt;php artisan migrate:fresh lt;span class=quot;ͼ12quot;gt;--seedlt;/spangt;lt;/codegt;lt;/pregt;lt;/divgt;lt;/divgt;lt;/divgt;lt;/divgt;lt;/divgt;lt;/divgt;lt;/divgt;lt;/divgt;lt;/divgt;lt;/divgt;lt;/divgt;lt;/divgt;lt;/divgt;lt;/divgt;lt;/pregt;lt;pgt;Trường hợp 2: nếu chưa có Category, tạo nhanh bằng Artisan:lt;/pgt;lt;pgt;Bashlt;/pgt;lt;pregt;lt;codegt;php artisan make:model Category -mcr
lt;/codegt;lt;/pregt;lt;pgt;Laravel sẽ tạo:lt;/pgt;lt;ulgt;lt;ligt;lt;pgt;lt;code inline=quot;quot;gt;app/Models/Category.phplt;/codegt;lt;/pgt;lt;/ligt;lt;ligt;lt;pgt;lt;code inline=quot;quot;gt;database/migrations/xxxx_create_categories_table.phplt;/codegt;lt;/pgt;lt;/ligt;lt;ligt;lt;pgt;lt;code inline=quot;quot;gt;app/Http/Controllers/CategoryController.phplt;/codegt;lt;/pgt;lt;/ligt;lt;/ulgt;lt;h3gt;Migrationlt;/h3gt;lt;pgt;PHPlt;/pgt;lt;pregt;lt;codegt;Schema::create(#39;categories#39;, function (Blueprint $table) {
$table-gt;id();
$table-gt;string(#39;name#39;);
$table-gt;string(#39;slug#39;)-gt;unique();
$table-gt;text(#39;description#39;)-gt;nullable();
$table-gt;timestamps();
});
lt;/codegt;lt;/pregt;lt;pgt;Chạy migration:lt;/pgt;lt;pgt;Bashlt;/pgt;lt;pregt;lt;codegt;php artisan migrate
lt;/codegt;lt;/pregt;lt;h3gt;2. Khai báo Modellt;/h3gt;lt;pgt;PHPlt;/pgt;lt;pregt;lt;codegt;namespace App\Models;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Factories\HasFactory;
class Category extends Model
{
use HasFactory;
protected $fillable = [
#39;name#39;,
#39;slug#39;,
#39;description#39;
];
}
lt;/codegt;lt;/pregt;lt;h3gt;3. Định nghĩa Resource Routelt;/h3gt;lt;pgt;Mở lt;code inline=quot;quot;gt;routes/web.phplt;/codegt;:lt;/pgt;lt;pgt;PHPlt;/pgt;lt;pregt;lt;codegt;use App\Http\Controllers\CategoryController;
Route::middleware([#39;auth#39;])-gt;group(function () {
Route::resource(#39;categories#39;, CategoryController::class);
});
lt;/codegt;lt;/pregt;lt;pgt;Laravel tự tạo 7 route chuẩn:lt;/pgt;lt;tablegt;lt;theadgt;lt;trgt;lt;thgt;Methodlt;/thgt;lt;thgt;URLlt;/thgt;lt;/trgt;lt;/theadgt;lt;tbodygt;lt;trgt;lt;tdgt;GETlt;/tdgt;lt;tdgt;/categorieslt;/tdgt;lt;/trgt;lt;trgt;lt;tdgt;GETlt;/tdgt;lt;tdgt;/categories/createlt;/tdgt;lt;/trgt;lt;trgt;lt;tdgt;POSTlt;/tdgt;lt;tdgt;/categorieslt;/tdgt;lt;/trgt;lt;trgt;lt;tdgt;GETlt;/tdgt;lt;tdgt;/categories/{quot;{category}quot;}lt;/tdgt;lt;/trgt;lt;trgt;lt;tdgt;GETlt;/tdgt;lt;tdgt;/categories/{quot;{category}quot;}/editlt;/tdgt;lt;/trgt;lt;trgt;lt;tdgt;PUTlt;/tdgt;lt;tdgt;/categories/{quot;{category}quot;}lt;/tdgt;lt;/trgt;lt;trgt;lt;tdgt;DELETElt;/tdgt;lt;tdgt;/categories/{quot;{category}quot;}lt;/tdgt;lt;/trgt;lt;/tbodygt;lt;/tablegt;lt;h3gt;4. Hiển thị danh sách Categories (---)lt;/h3gt;lt;h3gt;Controllerlt;/h3gt;lt;pgt;PHP (pagination bằng Laravel)lt;/pgt;lt;pregt;lt;codegt;use App\Models\Category;
use Illuminate\Http\Request;
public function index(Request $request)
{
$categories = Category::query()
-gt;when($request-gt;keyword, function ($query, $keyword) {
$query-gt;where(#39;name#39;, #39;like#39;, quot;%{$keyword}%quot;);
})
-gt;latest()
-gt;paginate(10)
-gt;withQueryString();
return view(#39;categories.index#39;, compact(#39;categories#39;));
}
lt;/codegt;lt;/pregt;lt;h3gt;View: resources/views/categories/index.blade.phplt;/h3gt;lt;pgt;PHPlt;/pgt;lt;pregt;lt;codegt;lt;x-app-layoutgt;
lt;div class=quot;max-w-7xl mx-auto p-6quot;gt;
lt;div class=quot;flex justify-between items-center mb-6quot;gt;
lt;h1 class=quot;text-2xl font-boldquot;gt;Quản lý Categorieslt;/h1gt;
lt;a href=quot;{{ route(#39;categories.create#39;) }}quot;
class=quot;bg-blue-600 text-white px-4 py-2 rounded-lg hover:bg-blue-700quot;gt;
+ Thêm Category
lt;/agt;
lt;/divgt;
lt;form method=quot;GETquot; class=quot;mb-4quot;gt;
lt;input type=quot;textquot;
name=quot;keywordquot;
value=quot;{{ request(#39;keyword#39;) }}quot;
placeholder=quot;Tìm kiếm category...quot;
class=quot;border rounded-lg px-4 py-2 w-80quot;gt;
lt;button class=quot;bg-gray-800 text-white px-4 py-2 rounded-lgquot;gt;
Tìm
lt;/buttongt;
lt;/formgt;
lt;div class=quot;bg-white rounded-xl shadow overflow-hiddenquot;gt;
lt;table class=quot;min-w-fullquot;gt;
lt;thead class=quot;bg-gray-100quot;gt;
lt;trgt;
lt;th class=quot;px-4 py-3 text-leftquot;gt;IDlt;/thgt;
lt;th class=quot;px-4 py-3 text-leftquot;gt;Tênlt;/thgt;
lt;th class=quot;px-4 py-3 text-leftquot;gt;Sluglt;/thgt;
lt;th class=quot;px-4 py-3 text-leftquot;gt;Ngày tạolt;/thgt;
lt;th class=quot;px-4 py-3 text-rightquot;gt;Thao táclt;/thgt;
lt;/trgt;
lt;/theadgt;
lt;tbody class=quot;divide-yquot;gt;
@forelse($categories as $category)
lt;tr class=quot;hover:bg-gray-50quot;gt;
lt;td class=quot;px-4 py-3quot;gt;{{ $category-gt;id }}lt;/tdgt;
lt;td class=quot;px-4 py-3 font-mediumquot;gt;
{{ $category-gt;name }}
lt;/tdgt;
lt;td class=quot;px-4 py-3 text-gray-600quot;gt;
{{ $category-gt;slug }}
lt;/tdgt;
lt;td class=quot;px-4 py-3quot;gt;
{{ $category-gt;created_at-gt;format(#39;d/m/Y#39;) }}
lt;/tdgt;
lt;td class=quot;px-4 py-3 text-right space-x-2quot;gt;
lt;a href=quot;{{ route(#39;categories.edit#39;, $category) }}quot;
class=quot;text-blue-600 hover:underlinequot;gt;
Sửa
lt;/agt;
lt;form action=quot;{{ route(#39;categories.destroy#39;, $category) }}quot;
method=quot;POSTquot;
class=quot;inlinequot;gt;
@csrf
@method(#39;DELETE#39;)
lt;button
onclick=quot;return confirm(#39;Xóa category này?#39;)quot;
class=quot;text-red-600 hover:underlinequot;gt;
Xóa
lt;/buttongt;
lt;/formgt;
lt;/tdgt;
lt;/trgt;
@empty
lt;trgt;
lt;td colspan=quot;5quot; class=quot;text-center py-8 text-gray-500quot;gt;
Chưa có dữ liệu
lt;/tdgt;
lt;/trgt;
@endforelse
lt;/tbodygt;
lt;/tablegt;
lt;/divgt;
lt;div class=quot;mt-6quot;gt;
{{ $categories-gt;links() }}
lt;/divgt;
lt;/divgt;
lt;/x-app-layoutgt;
lt;/codegt;lt;/pregt;lt;h3gt;5. Tạo Category mớilt;/h3gt;lt;h3gt;Form Createlt;/h3gt;lt;pgt;PHPlt;/pgt;lt;pregt;lt;codegt;lt;form action=quot;{{ route(#39;categories.store#39;) }}quot; method=quot;POSTquot;gt;
@csrf
lt;div class=quot;mb-4quot;gt;
lt;label class=quot;block mb-1quot;gt;Tên Categorylt;/labelgt;
lt;input type=quot;textquot;
name=quot;namequot;
value=quot;{{ old(#39;name#39;) }}quot;
class=quot;w-full border rounded-lg px-4 py-2quot;gt;
@error(#39;name#39;)
lt;p class=quot;text-red-500 text-sm mt-1quot;gt;{{ $message }}lt;/pgt;
@enderror
lt;/divgt;
lt;div class=quot;mb-4quot;gt;
lt;label class=quot;block mb-1quot;gt;Sluglt;/labelgt;
lt;input type=quot;textquot;
name=quot;slugquot;
value=quot;{{ old(#39;slug#39;) }}quot;
class=quot;w-full border rounded-lg px-4 py-2quot;gt;
lt;/divgt;
lt;div class=quot;mb-4quot;gt;
lt;label class=quot;block mb-1quot;gt;Mô tảlt;/labelgt;
lt;textarea name=quot;descriptionquot;
class=quot;w-full border rounded-lg px-4 py-2quot;
rows=quot;4quot;gt;{{ old(#39;description#39;) }}lt;/textareagt;
lt;/divgt;
lt;button class=quot;bg-blue-600 text-white px-6 py-2 rounded-lgquot;gt;
Lưu
lt;/buttongt;
lt;/formgt;
lt;/codegt;lt;pgt;lt;br /gt;lt;/pgt;lt;/pregt;lt;h3gt;Controller Storelt;/h3gt;lt;pgt;PHPlt;/pgt;lt;pregt;lt;codegt;public function store(Request $request)
{
$validated = $request-gt;validate([
#39;name#39; =gt; #39;required|min:2|max:100#39;,
#39;slug#39; =gt; #39;required|unique:categories,slug#39;,
#39;description#39; =gt; #39;nullable|max:1000#39;,
]);
Category::create($validated);
return redirect()
-gt;route(#39;categories.index#39;)
-gt;with(#39;success#39;, #39;Thêm category thành công!#39;);
}
lt;/codegt;lt;/pregt;lt;h3gt;6. Cập nhật Categorylt;/h3gt;lt;h3gt;Controller Editlt;/h3gt;lt;pgt;PHPlt;/pgt;lt;pregt;lt;codegt;public function edit(Category $category)
{
return view(#39;categories.edit#39;, compact(#39;category#39;));
}
lt;/codegt;lt;/pregt;lt;h3gt;Controller Updatelt;/h3gt;lt;pgt;PHPlt;/pgt;lt;pregt;lt;codegt;public function update(Request $request, Category $category)
{
$validated = $request-gt;validate([
#39;name#39; =gt; #39;required|min:2|max:100#39;,
#39;slug#39; =gt; #39;required|unique:categories,slug,#39; . $category-gt;id,
#39;description#39; =gt; #39;nullable|max:1000#39;,
]);
$category-gt;update($validated);
return redirect()
-gt;route(#39;categories.index#39;)
-gt;with(#39;success#39;, #39;Cập nhật thành công!#39;);
}
lt;/codegt;lt;/pregt;lt;pgt;Điểm quan trọng là rule:lt;/pgt;lt;pgt;PHPlt;/pgt;lt;pregt;lt;codegt;#39;slug#39; =gt; #39;required|unique:categories,slug,#39; . $category-gt;id
lt;/codegt;lt;/pregt;lt;pgt;Đ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.lt;/pgt;lt;h3gt;7. Xóa Categorylt;/h3gt;lt;pgt;PHPlt;/pgt;lt;pregt;lt;codegt;public function destroy(Category $category)
{
$category-gt;delete();
return redirect()
-gt;route(#39;categories.index#39;)
-gt;with(#39;success#39;, #39;Đã xóa category!#39;);
}
lt;/codegt;lt;/pregt;lt;pgt;Trong bài 30 chúng ta sẽ nâng cấp thành Soft Delete.lt;/pgt;lt;h3gt;8. Tìm kiếm Categorieslt;/h3gt;lt;pgt;Chức năng tìm kiếm đã được tích hợp trong phương thức lt;code inline=quot;quot;gt;index()lt;/codegt;:lt;/pgt;lt;pgt;PHPlt;/pgt;lt;pregt;lt;codegt;-gt;when($request-gt;keyword, function ($query, $keyword) {
$query-gt;where(#39;name#39;, #39;like#39;, quot;%{$keyword}%quot;);
})
lt;/codegt;lt;/pregt;lt;pgt;Ví dụ:lt;/pgt;lt;ulgt;lt;ligt;lt;pgt;Nhập lt;code inline=quot;quot;gt;laravellt;/codegt; → hiển thị các category chứa từ “laravel”.lt;/pgt;lt;/ligt;lt;ligt;lt;pgt;Nhập lt;code inline=quot;quot;gt;phplt;/codegt; → hiển thị các category liên quan PHP.lt;/pgt;lt;/ligt;lt;ligt;lt;pgt;Để trống → hiển thị toàn bộ dữ liệu.lt;/pgt;lt;/ligt;lt;/ulgt;lt;pgt;Nhờ lt;code inline=quot;quot;gt;withQueryString()lt;/codegt;, khi chuyển trang Laravel vẫn giữ lại từ khóa tìm kiếm.lt;/pgt;lt;h3gt;9. Phân tranglt;/h3gt;lt;pgt;PHPlt;/pgt;lt;pregt;lt;codegt;-gt;paginate(10)
lt;/codegt;lt;/pregt;lt;pgt;Hiển thị:lt;/pgt;lt;pgt;PHPlt;/pgt;lt;pregt;lt;codegt;{{ $categories-gt;links() }}
lt;/codegt;lt;/pregt;lt;pgt;Laravel 12 mặc định sử dụng giao diện phân trang tương thích Tailwind CSS.lt;/pgt;lt;h3gt;10. Hiển thị thông báo thành cônglt;/h3gt;lt;pgt;Thêm vào đầu trang:lt;/pgt;lt;pgt;PHPlt;/pgt;lt;pregt;lt;codegt;@if(session(#39;success#39;))
lt;div class=quot;mb-4 rounded-lg bg-green-100 border border-green-300
text-green-800 px-4 py-3quot;gt;
{{ session(#39;success#39;) }}
lt;/divgt;
@endif
lt;/codegt;lt;/pregt;lt;pgt;Kết quả:lt;/pgt;lt;ulgt;lt;ligt;lt;pgt;Thêm mới → “Thêm category thành công!”lt;/pgt;lt;/ligt;lt;ligt;lt;pgt;Cập nhật → “Cập nhật thành công!”lt;/pgt;lt;/ligt;lt;ligt;lt;pgt;Xóa → “Đã xóa category!”lt;/pgt;lt;/ligt;lt;/ulgt;lt;h3gt;11. Cấu trúc thư mục sau khi hoàn thànhlt;/h3gt;lt;pregt;lt;codegt;app/
├── Http/
│ └── Controllers/
│ └── CategoryController.php
├── Models/
│ └── Category.php
resources/
└── views/
└── categories/
├── index.blade.php
├── create.blade.php
└── edit.blade.php
routes/
└── web.php
lt;/codegt;lt;/pregt;lt;h3gt;12. Kết quả đạt đượclt;/h3gt;lt;pgt;Sau bài học này, module Quản lý Categories của Blog CMS đã có đầy đủ:lt;/pgt;lt;pgt;CRUDlt;/pgt;lt;pgt;Quản lý dữ liệult;/pgt;lt;ulgt;lt;ligt;lt;pgt;Danh sách Categorieslt;/pgt;lt;/ligt;lt;ligt;lt;pgt;Thêm mới Categorylt;/pgt;lt;/ligt;lt;ligt;lt;pgt;Cập nhật Categorylt;/pgt;lt;/ligt;lt;ligt;lt;pgt;Xóa Categorylt;/pgt;lt;/ligt;lt;/ulgt;lt;pgt;Validationlt;/pgt;lt;pgt;Kiểm tra dữ liệult;/pgt;lt;ulgt;lt;ligt;lt;pgt;Validate tên danh mụclt;/pgt;lt;/ligt;lt;ligt;lt;pgt;Validate slug duy nhấtlt;/pgt;lt;/ligt;lt;ligt;lt;pgt;Hiển thị lỗi ngay trên formlt;/pgt;lt;/ligt;lt;/ulgt;lt;pgt;Searchlt;/pgt;lt;pgt;Tìm kiếmlt;/pgt;lt;ulgt;lt;ligt;lt;pgt;Lọc theo tên categorylt;/pgt;lt;/ligt;lt;ligt;lt;pgt;Giữ từ khóa khi chuyển tranglt;/pgt;lt;/ligt;lt;/ulgt;lt;pgt;Paginationlt;/pgt;lt;pgt;Phân tranglt;/pgt;lt;ulgt;lt;ligt;lt;pgt;10 bản ghi mỗi tranglt;/pgt;lt;/ligt;lt;ligt;lt;pgt;Giao diện Tailwind mặc định của Laravel 12lt;/pgt;lt;/ligt;lt;/ulgt;lt;pgt;Đâ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.lt;/pgt;lt;h3gt;Bài tiếp theolt;/h3gt;lt;pgt;Sang Bài 27 — CRUD Posts, chúng ta sẽ kết hợp:lt;/pgt;lt;ulgt;lt;ligt;lt;pgt;Relationship giữa Post và Category.lt;/pgt;lt;/ligt;lt;ligt;lt;pgt;Gán bài viết cho User.lt;/pgt;lt;/ligt;lt;ligt;lt;pgt;Upload hình ảnh.lt;/pgt;lt;/ligt;lt;ligt;lt;pgt;Validate dữ liệu phức tạp hơn.lt;/pgt;lt;/ligt;lt;ligt;lt;pgt;Hiển thị danh sách bài viết kèm tên danh mục và tác giả.lt;/pgt;lt;/ligt;lt;/ulgt;lt;pgt;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ế.lt;/pgt;lt;pgt;lt;img alt=quot;03 - Categories Table with Livewire | Laravel Dailyquot; height=quot;277quot; src=quot;https://images.openai.com/static-rsc-4/sRgXxdO4bWka6lah4S9gGY9UykLuZppGHRAKV_AzjknarfMymuHmvpO1s7wzrcza_1h6uFjt1Yg-mCx9iz3hKGixmUEuc3ECeOMZmgE0D84FxDBzx_K0JOL1ZlQpUZKnBvdUJr3QbjEGGqArPWQkrbGHgTb9b7jGOG8ALUG7T5r8wfQNSkhTOfY54HOfxcRz?purpose=fullsizequot; style=quot;font-family: monospace; white-space: pre;quot; width=quot;320quot; /gt;lt;/pgt;