lt;pgt;Sau khi hoàn thành module lt;stronggt;Categorieslt;/stronggt;, chúng ta sẽ xây dựng chức năng quan trọng nhất của Blog CMS: lt;stronggt;quản lý bài viết (Posts)lt;/stronggt;.lt;/pgt;lt;pgt;Khác với Category chỉ có vài trường dữ liệu, Post sẽ liên kết với nhiều bảng khác nhau như lt;stronggt;Categorylt;/stronggt;, lt;stronggt;Userlt;/stronggt;, đồng thời hỗ trợ lt;stronggt;upload hình ảnhlt;/stronggt;, kiểm tra dữ liệu đầu vào và hiển thị danh sách bài viết chuyên nghiệp.lt;/pgt;lt;pgt;Đây cũng là module có số lượng kiến thức nhiều nhất trong toàn bộ dự án.lt;/pgt;lt;hr /gt;lt;h1gt;Mục tiêu bài họclt;/h1gt;lt;pgt;Sau bài này bạn sẽ thực hiện được:lt;/pgt;lt;ulgt;lt;ligt;lt;pgt;CRUD bài viếtlt;/pgt;lt;/ligt;lt;ligt;lt;pgt;Upload ảnh đại diệnlt;/pgt;lt;/ligt;lt;ligt;lt;pgt;Chọn Categorylt;/pgt;lt;/ligt;lt;ligt;lt;pgt;Gán tác giả (User)lt;/pgt;lt;/ligt;lt;ligt;lt;pgt;Validation dữ liệult;/pgt;lt;/ligt;lt;ligt;lt;pgt;Hiển thị Relationshiplt;/pgt;lt;/ligt;lt;ligt;lt;pgt;Tự động tạo sluglt;/pgt;lt;/ligt;lt;ligt;lt;pgt;Phân trang dữ liệult;/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/AVvXsEj-N_39djC6QL25iMVnO00eB4LwOn_hTWeu1Nlppifi8Z1Js5KhAkmfY-4gwDF9OBHEE2poQL2mNThb17EIEjmCWFz1F1FD2Og7Pk0bEdELmATyZ60WXi5CWe_P-2JVl1IuAcndIL2I0eUyG87Pwyb0nouJ6UmheKu1prL5ULhDJ2QCQdiL_6V_VjeXs4w/s1536/laravel-rung.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/AVvXsEj-N_39djC6QL25iMVnO00eB4LwOn_hTWeu1Nlppifi8Z1Js5KhAkmfY-4gwDF9OBHEE2poQL2mNThb17EIEjmCWFz1F1FD2Og7Pk0bEdELmATyZ60WXi5CWe_P-2JVl1IuAcndIL2I0eUyG87Pwyb0nouJ6UmheKu1prL5ULhDJ2QCQdiL_6V_VjeXs4w/s320/laravel-rung.pngquot; width=quot;320quot; /gt;lt;/agt;lt;spangt;lt;a name=#39;more#39;gt;lt;/agt;lt;/spangt;lt;/divgt;lt;pgt;lt;/pgt;lt;/ligt;lt;/ulgt;lt;hr /gt;lt;h1gt;Kết quả sau bài họclt;/h1gt;lt;pgt;Danh sách bài viết sẽ hiển thị như sau:lt;/pgt;lt;tablegt;lt;theadgt;lt;trgt;lt;thgt;Ảnhlt;/thgt;lt;thgt;Tiêu đềlt;/thgt;lt;thgt;Categorylt;/thgt;lt;thgt;Tác giảlt;/thgt;lt;thgt;Ngày đănglt;/thgt;lt;thgt;Thao táclt;/thgt;lt;/trgt;lt;/theadgt;lt;tbodygt;lt;trgt;lt;tdgt;✔lt;/tdgt;lt;tdgt;Laravel 12 mới có gì?lt;/tdgt;lt;tdgt;Laravellt;/tdgt;lt;tdgt;Adminlt;/tdgt;lt;tdgt;07/08/2026lt;/tdgt;lt;tdgt;Edit / Deletelt;/tdgt;lt;/trgt;lt;/tbodygt;lt;/tablegt;lt;hr /gt;lt;h1gt;1. Cấu trúc Databaselt;/h1gt;lt;pgt;Bảng lt;stronggt;postslt;/stronggt;lt;/pgt;lt;tablegt;lt;theadgt;lt;trgt;lt;thgt;Fieldlt;/thgt;lt;thgt;Kiểult;/thgt;lt;/trgt;lt;/theadgt;lt;tbodygt;lt;trgt;lt;tdgt;idlt;/tdgt;lt;tdgt;bigintlt;/tdgt;lt;/trgt;lt;trgt;lt;tdgt;user_idlt;/tdgt;lt;tdgt;foreignIdlt;/tdgt;lt;/trgt;lt;trgt;lt;tdgt;category_idlt;/tdgt;lt;tdgt;foreignIdlt;/tdgt;lt;/trgt;lt;trgt;lt;tdgt;titlelt;/tdgt;lt;tdgt;stringlt;/tdgt;lt;/trgt;lt;trgt;lt;tdgt;sluglt;/tdgt;lt;tdgt;stringlt;/tdgt;lt;/trgt;lt;trgt;lt;tdgt;imagelt;/tdgt;lt;tdgt;stringlt;/tdgt;lt;/trgt;lt;trgt;lt;tdgt;excerptlt;/tdgt;lt;tdgt;textlt;/tdgt;lt;/trgt;lt;trgt;lt;tdgt;contentlt;/tdgt;lt;tdgt;longTextlt;/tdgt;lt;/trgt;lt;trgt;lt;tdgt;created_atlt;/tdgt;lt;tdgt;timestamplt;/tdgt;lt;/trgt;lt;/tbodygt;lt;/tablegt;lt;pgt;Quan hệlt;/pgt;lt;pregt;lt;codegt;User
│
├──────lt; Posts gt;────── Category
lt;/codegt;lt;/pregt;
lt;pgt;Một User có nhiều Post.lt;/pgt;lt;pgt;Một Category có nhiều Post.lt;/pgt;lt;pgt;Một Post chỉ thuộc một User và một Category.lt;/pgt;lt;pgt;Chú ý:lt;/pgt;
lt;h3gt;
lt;spangt;1. Sửa migration lt;codegt;create_posts_tablelt;/codegt;lt;/spangt;
lt;/h3gt;
lt;pgt;Đưa luôn cấu trúc hoàn chỉnh vào migration tạo lt;codegt;postslt;/codegt;:lt;/pgt;
lt;pregt;lt;codegt;Schema::create(#39;posts#39;, function (Blueprint $table) {
$table-gt;id();
$table-gt;foreignId(#39;user_id#39;)
-gt;constrained()
-gt;cascadeOnDelete();
$table-gt;foreignId(#39;category_id#39;)
-gt;nullable()
-gt;constrained()
-gt;nullOnDelete();
$table-gt;string(#39;title#39;);
$table-gt;string(#39;slug#39;)
-gt;unique();
$table-gt;string(#39;image#39;)
-gt;nullable();
$table-gt;text(#39;excerpt#39;)
-gt;nullable();
$table-gt;longText(#39;content#39;);
$table-gt;timestamps();
});lt;/codegt;lt;/pregt;
lt;pgt;Như vậy bạn lt;stronggt;không cần 3 migration riênglt;/stronggt; để:lt;/pgt;
lt;ulgt;
lt;ligt;
thêm lt;codegt;category_idlt;/codegt;
lt;/ligt;
lt;ligt;
thêm lt;codegt;imagelt;/codegt;
lt;/ligt;
lt;ligt;
thêm lt;codegt;sluglt;/codegt;
lt;/ligt;
lt;ligt;
thêm lt;codegt;excerptlt;/codegt;
lt;/ligt;
lt;/ulgt;
lt;pgt;nữa, nếu đây vẫn là database đang phát triển và chưa cần bảo toàn dữ liệu production.lt;/pgt;
lt;hr /gt;
lt;h3gt;2. Xóa các migration bổ sung cũlt;/h3gt;
lt;pgt;Nếu bạn đã có các migration kiểu:lt;/pgt;
lt;pregt;lt;codegt;add_category_id_to_posts_table
add_image_to_posts_table
add_slug_and_excerpt_to_posts_tablelt;/codegt;lt;/pregt;
lt;pgt;thì có thể xóa chúng lt;stronggt;sau khi đã đưa cấu trúc cuối cùng vào migration lt;codegt;create_posts_tablelt;/codegt;lt;/stronggt;.lt;/pgt;
lt;pgt;Mục tiêu là migration tạo bảng lt;codegt;postslt;/codegt; ngay từ đầu đã hoàn chỉnh.lt;/pgt;
lt;h3gt;3. Sửa Post modellt;/h3gt;
lt;codegt;app/Models/Post.phplt;/codegt;
lt;pregt;lt;codegt;lt;?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
class Post extends Model
{
protected $fillable = [
#39;user_id#39;,
#39;category_id#39;,
#39;title#39;,
#39;slug#39;,
#39;image#39;,
#39;excerpt#39;,
#39;content#39;,
];
}lt;/codegt;lt;/pregt;
lt;h3gt;4. Seeder sửa Factorylt;/h3gt;
lt;pregt;lt;codegt;lt;?php
namespace Database\Factories;
use App\Models\Category;
use App\Models\User;
use Illuminate\Database\Eloquent\Factories\Factory;
class PostFactory extends Factory
{
public function definition(): array
{
return [
// Định nghĩa dữ liệu mẫu
#39;title#39; =gt; fake()-gt;sentence(),
#39;slug#39; =gt; fake()-gt;unique()-gt;slug(),
#39;image#39; =gt; fake()-gt;imageUrl(1200, 800),
#39;excerpt#39; =gt; fake()-gt;paragraph(),
#39;content#39; =gt; fake()-gt;paragraphs(5, true),
#39;user_id#39; =gt; fn () =gt;
User::query()-gt;inRandomOrder()-gt;value(#39;id#39;),
#39;category_id#39; =gt; fn () =gt;
Category::query()-gt;inRandomOrder()-gt;value(#39;id#39;),
];
}
}lt;/codegt;lt;/pregt;
lt;divgt;
lt;pgt;lt;ugt;Chú ýlt;/ugt;: phải tạo bảng chứa khóa ngoại trước nếu không sẽ báo lỗi.lt;/pgt;
lt;pgt;thứ tự migration nên là:lt;/pgt;
lt;pregt;lt;codegt;1. create_countries_table
2. create_users_table
3. create_categories_table
4. create_posts_tablelt;/codegt;lt;/pregt;
lt;pgt;Sau đó chạy:lt;/pgt;
lt;pregt;lt;codegt;php artisan migrate:fresh --seedlt;/codegt;lt;/pregt;
lt;hr /gt;
lt;h1gt;2. Relationshiplt;/h1gt;
lt;h2gt;User.phplt;/h2gt;
lt;/divgt;
lt;pregt;lt;code class=quot;language-phpquot;gt;public function posts()
{
return $this-gt;hasMany(Post::class);
}
lt;/codegt;lt;/pregt;lt;hr /gt;lt;h2gt;Category.phplt;/h2gt;
lt;pregt;lt;code class=quot;language-phpquot;gt;public function posts()
{
return $this-gt;hasMany(Post::class);
}
lt;/codegt;lt;/pregt;lt;hr /gt;lt;h2gt;Post.phplt;/h2gt;
lt;pregt;lt;code class=quot;language-phpquot;gt;public function category()
{
return $this-gt;belongsTo(Category::class);
}
public function user()
{
return $this-gt;belongsTo(User::class);
}
lt;/codegt;lt;/pregt;lt;pgt;Đây là ba relationship được sử dụng xuyên suốt dự án.lt;/pgt;lt;hr /gt;lt;h1gt;3. Resource Controllerlt;/h1gt;lt;pgt;Tạo controllerlt;/pgt;
lt;pregt;lt;code class=quot;language-bashquot;gt;php artisan make:controller PostController --resource
lt;/codegt;lt;/pregt;lt;pgt;Đăng ký routelt;/pgt;
lt;pregt;lt;code class=quot;language-phpquot;gt;Route::resource(#39;posts#39;, PostController::class);
lt;/codegt;lt;/pregt;lt;pgt;Laravel sẽ tạo sẵn:lt;/pgt;lt;pregt;lt;codegt;index
create
store
show
edit
update
destroy
lt;/codegt;lt;/pregt;lt;hr /gt;lt;h1gt;4. Hiển thị danh sách bài viếtlt;/h1gt;lt;pgt;Controllerlt;/pgt;lt;pregt;lt;code class=quot;language-phpquot;gt;public function index()
{
//dd(Auth::user());
//lấy sẵn user để sau này chặn ngay từ view chỉ có admin được xem all posts
$user = Auth::user();
$posts = Post::with([#39;category#39;,#39;user#39;])
-gt;latest()
-gt;paginate(10);
return view(#39;posts.index#39;, compact(#39;posts#39;));
}
lt;/codegt;lt;/pregt;lt;pgt;Điểm quan trọnglt;/pgt;lt;pregt;lt;codegt;with()
lt;/codegt;lt;/pregt;lt;pgt;Laravel sẽ eager loading.lt;/pgt;lt;pgt;Không xảy ra lỗi lt;stronggt;N+1 Querylt;/stronggt;.lt;/pgt;lt;pgt;lt;ugt;Chú ýlt;/ugt;: nhớ kiểm tra route list trước coi đủ 7 món hay không.lt;/pgt;lt;pre class=quot;overflow-visible! px-0!quot; data-end=quot;851quot; data-start=quot;813quot;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 optimize:clearlt;/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;862quot; data-start=quot;853quot;gt;Kiểm tra:lt;/pgt;lt;pgt;
lt;/pgt;lt;pre class=quot;overflow-visible! px-0!quot; data-end=quot;911quot; data-start=quot;864quot;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 route:list lt;span class=quot;ͼ12quot;gt;--namelt;/spangt;lt;span class=quot;ͼvquot;gt;=lt;/spangt;postslt;/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;h3 class=quot;PDq2pG_selectionAnchorContainerquot; data-end=quot;600quot; data-section-id=quot;1fmeyxpquot; data-start=quot;583quot;gt;lt;span role=quot;textquot;gt;lt;code data-end=quot;600quot; data-start=quot;587quot;gt;posts.indexlt;/codegt;lt;/spangt;lt;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;4694quot; data-start=quot;602quot;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;lt;x-app-layoutgt;
lt;div class=quot;py-6quot;gt;
lt;div class=quot;mx-auto max-w-7xl px-4 sm:px-6 lg:px-8quot;gt;
lt;!-- Header --gt;
lt;div class=quot;mb-6 flex items-center justify-betweenquot;gt;
lt;divgt;
lt;h2 class=quot;text-2xl font-bold text-gray-800quot;gt;
Quản lý bài viết
lt;/h2gt;
lt;p class=quot;mt-1 text-sm text-gray-500quot;gt;
Danh sách tất cả bài viết trong hệ thống
lt;/pgt;
lt;/divgt;
lt;a href=quot;{{ route(#39;posts.create#39;) }}quot;
class=quot;inline-flex items-center rounded-lg bg-indigo-600 px-4 py-2.5
text-sm font-semibold text-white shadow-sm
hover:bg-indigo-700quot;gt;
+ Thêm bài viết
lt;/agt;
lt;/divgt;
lt;!-- Card --gt;
lt;div class=quot;overflow-hidden rounded-xl bg-white shadow-sm ring-1 ring-gray-200quot;gt;
lt;!-- Card Header --gt;
lt;div class=quot;border-b border-gray-200 px-6 py-4quot;gt;
lt;div class=quot;flex items-center justify-betweenquot;gt;
lt;divgt;
lt;h3 class=quot;text-lg font-semibold text-gray-800quot;gt;
Danh sách bài viết
lt;/h3gt;
lt;p class=quot;text-sm text-gray-500quot;gt;
Tổng cộng {{ $posts-gt;total() }} bài viết
lt;/pgt;
lt;/divgt;
lt;/divgt;
lt;/divgt;
lt;!-- Table --gt;
lt;div class=quot;overflow-x-autoquot;gt;
lt;table class=quot;min-w-full divide-y divide-gray-200quot;gt;
lt;thead class=quot;bg-gray-50quot;gt;
lt;trgt;
lt;th class=quot;px-6 py-3 text-left text-xs font-semibold uppercase tracking-wider text-gray-500quot;gt;
Tiêu đề
lt;/thgt;
lt;th class=quot;px-6 py-3 text-left text-xs font-semibold uppercase tracking-wider text-gray-500quot;gt;
Ngày
lt;/thgt;
lt;th class=quot;px-6 py-3 text-left text-xs font-semibold uppercase tracking-wider text-gray-500quot;gt;
Tác giả
lt;/thgt;
lt;th class=quot;px-6 py-3 text-left text-xs font-semibold uppercase tracking-wider text-gray-500quot;gt;
Danh mục
lt;/thgt;
lt;th class=quot;px-6 py-3 text-right text-xs font-semibold uppercase tracking-wider text-gray-500quot;gt;
Thao tác
lt;/thgt;
lt;/trgt;
lt;/theadgt;
lt;tbody class=quot;divide-y divide-gray-200 bg-whitequot;gt;
@forelse($posts as $post)
lt;tr class=quot;hover:bg-gray-50quot;gt;
{{-- TITLE --}}
lt;td class=quot;px-6 py-4quot;gt;
lt;a href=quot;{{ route(#39;posts.show#39;, $post) }}quot;
class=quot;font-semibold text-gray-800 hover:text-indigo-600quot;gt;
{{ $post-gt;title }}
lt;/agt;
lt;/tdgt;
{{-- DATE --}}
lt;td class=quot;whitespace-nowrap px-6 py-4 text-sm text-gray-500quot;gt;
{{ $post-gt;created_at-gt;format(#39;d/m/Y#39;) }}
lt;/tdgt;
{{-- AUTHOR --}}
lt;td class=quot;whitespace-nowrap px-6 py-4quot;gt;
@if($post-gt;user)
lt;span class=quot;text-sm font-medium text-gray-700quot;gt;
{{ $post-gt;user-gt;name }}
lt;/spangt;
@else
lt;span class=quot;text-sm text-gray-400quot;gt;
—
lt;/spangt;
@endif
lt;/tdgt;
{{-- CATEGORY --}}
lt;td class=quot;whitespace-nowrap px-6 py-4quot;gt;
@if($post-gt;category)
lt;span class=quot;inline-flex rounded-full
bg-blue-100 px-3 py-1
text-xs font-medium text-blue-700quot;gt;
{{ $post-gt;category-gt;name }}
lt;/spangt;
@else
lt;span class=quot;text-sm text-gray-400quot;gt;
—
lt;/spangt;
@endif
lt;/tdgt;
{{-- ACTIONS --}}
lt;td class=quot;whitespace-nowrap px-6 py-4quot;gt;
lt;div class=quot;flex justify-end gap-2quot;gt;
{{-- VIEW --}}
lt;a href=quot;{{ route(#39;posts.show#39;, $post) }}quot;
class=quot;rounded-lg bg-gray-100 px-3 py-2
text-sm font-medium text-gray-700
hover:bg-gray-200quot;gt;
Xem
lt;/agt;
{{-- EDIT --}}
lt;a href=quot;{{ route(#39;posts.edit#39;, $post) }}quot;
class=quot;rounded-lg bg-blue-100 px-3 py-2
text-sm font-medium text-blue-700
hover:bg-blue-200quot;gt;
Sửa
lt;/agt;
{{-- DELETE --}}
lt;form action=quot;{{ route(#39;posts.destroy#39;, $post) }}quot;
method=quot;POSTquot;
onsubmit=quot;return confirm(#39;Bạn có chắc muốn xóa bài viết này?#39;);quot;gt;
@csrf
@method(#39;DELETE#39;)
lt;button type=quot;submitquot;
class=quot;rounded-lg bg-red-100 px-3 py-2
text-sm font-medium text-red-700
hover:bg-red-200quot;gt;
Xóa
lt;/buttongt;
lt;/formgt;
lt;/divgt;
lt;/tdgt;
lt;/trgt;
@empty
lt;trgt;
lt;td colspan=quot;5quot;
class=quot;px-6 py-12 text-center text-sm text-gray-400quot;gt;
Chưa có bài viết nào.
lt;/tdgt;
lt;/trgt;
@endforelse
lt;/tbodygt;
lt;/tablegt;
lt;/divgt;
lt;!-- Pagination --gt;
@if($posts-gt;hasPages())
lt;div class=quot;border-t border-gray-200 bg-white px-6 py-4quot;gt;
{{ $posts-gt;links() }}
lt;/divgt;
@endif
lt;/divgt;
lt;/divgt;
lt;/divgt;
lt;/x-app-layoutgt;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;hr /gt;lt;h1gt;5. Hiển thị Relationshiplt;/h1gt;lt;pgt;Trong Bladelt;/pgt;lt;pregt;lt;code class=quot;language-bladequot;gt;{{ $post-gt;category-gt;name }}
lt;/codegt;lt;/pregt;lt;pregt;lt;code class=quot;language-bladequot;gt;{{ $post-gt;user-gt;name }}
lt;/codegt;lt;/pregt;lt;pgt;Không cần viết Query JOIN.lt;/pgt;lt;pgt;Đó chính là sức mạnh của Eloquent ORM.lt;/pgt;lt;hr /gt;lt;h1gt;6. Form tạo bài viết(---)lt;/h1gt;lt;pgt;Form sẽ có:lt;/pgt;lt;ulgt;lt;ligt;lt;pgt;Tiêu đềlt;/pgt;lt;/ligt;lt;ligt;lt;pgt;Sluglt;/pgt;lt;/ligt;lt;ligt;lt;pgt;Categorylt;/pgt;lt;/ligt;lt;ligt;lt;pgt;Hình ảnhlt;/pgt;lt;/ligt;lt;ligt;lt;pgt;Mô tả ngắnlt;/pgt;lt;/ligt;lt;ligt;lt;pgt;Nội dunglt;/pgt;lt;/ligt;lt;/ulgt;lt;pgt;Categorylt;/pgt;lt;pregt;lt;code class=quot;language-bladequot;gt;lt;select name=quot;category_idquot;gt;
@foreach($categories as $category)
lt;option value=quot;{{ $category-gt;id }}quot;gt;
{{ $category-gt;name }}
lt;/optiongt;
@endforeach
lt;/selectgt;
lt;/codegt;lt;/pregt;lt;pgt;Laravel sẽ lấy toàn bộ Category để người dùng lựa chọn.lt;/pgt;lt;hr /gt;lt;h1gt;7. Validationlt;/h1gt;lt;pregt;lt;code class=quot;language-phpquot;gt;$request-gt;validate([
#39;title#39;=gt;#39;required|max:255#39;,
#39;slug#39;=gt;#39;required|unique:posts#39;,
#39;category_id#39;=gt;#39;required|exists:categories,id#39;,
#39;image#39;=gt;#39;nullable|image|max:2048#39;,
#39;content#39;=gt;#39;required#39;
]);
lt;/codegt;lt;/pregt;lt;pgt;Laravel sẽ kiểm tra:lt;/pgt;lt;ulgt;lt;ligt;lt;pgt;tiêu đề bắt buộclt;/pgt;lt;/ligt;lt;ligt;lt;pgt;slug không được trùnglt;/pgt;lt;/ligt;lt;ligt;lt;pgt;category phải tồn tạilt;/pgt;lt;/ligt;lt;ligt;lt;pgt;chỉ nhận ảnhlt;/pgt;lt;/ligt;lt;ligt;lt;pgt;dung lượng tối đalt;/pgt;lt;/ligt;lt;ligt;lt;pgt;nội dung không được rỗnglt;/pgt;lt;/ligt;lt;/ulgt;lt;hr /gt;lt;h1gt;8. Upload hình ảnhlt;/h1gt;lt;pgt;Formlt;/pgt;lt;pregt;lt;code class=quot;language-bladequot;gt;enctype=quot;multipart/form-dataquot;
lt;/codegt;lt;/pregt;lt;pgt;Controllerlt;/pgt;lt;pregt;lt;code class=quot;language-phpquot;gt;$image = null;
if($request-gt;hasFile(#39;image#39;)){
$image = $request-gt;file(#39;image#39;)
-gt;store(#39;posts#39;,#39;public#39;);
}
lt;/codegt;lt;/pregt;lt;pgt;Kết quảlt;/pgt;lt;pregt;lt;codegt;storage/app/public/posts/
abc123.jpg
xyz456.png
lt;/codegt;lt;/pregt;lt;pgt;Laravel sẽ tự sinh tên file ngẫu nhiên để tránh trùng.lt;/pgt;lt;hr /gt;lt;h1gt;9. Lưu dữ liệult;/h1gt;lt;pregt;lt;code class=quot;language-phpquot;gt;Post::create([
#39;user_id#39;=gt;auth()-gt;id(),
#39;category_id#39;=gt;$request-gt;category_id,
#39;title#39;=gt;$request-gt;title,
#39;slug#39;=gt;$request-gt;slug,
#39;image#39;=gt;$image,
#39;excerpt#39;=gt;$request-gt;excerpt,
#39;content#39;=gt;$request-gt;content
]);
lt;/codegt;lt;/pregt;lt;pgt;Tác giả sẽ được lấy từlt;/pgt;lt;pregt;lt;code class=quot;language-phpquot;gt;auth()-gt;id()
lt;/codegt;lt;/pregt;lt;pgt;Người dùng không thể giả mạo tác giả bài viết.lt;/pgt;lt;hr /gt;lt;h1gt;10. Hiển thị ảnhlt;/h1gt;lt;pgt;Bladelt;/pgt;lt;pregt;lt;code class=quot;language-bladequot;gt;lt;img src=quot;{{ asset(#39;storage/#39;.$post-gt;image) }}quot;gt;
lt;/codegt;lt;/pregt;lt;pgt;Đừng quên tạo symbolic linklt;/pgt;lt;pregt;lt;code class=quot;language-bashquot;gt;php artisan storage:link
lt;/codegt;lt;/pregt;lt;pgt;Sau khi chạy lệnh này, Laravel sẽ tạolt;/pgt;lt;pregt;lt;codegt;public/storage
lt;/codegt;lt;/pregt;lt;pgt;trỏ tớilt;/pgt;lt;pregt;lt;codegt;storage/app/public
lt;/codegt;lt;/pregt;lt;hr /gt;lt;h1gt;11. Cập nhật bài viếtlt;/h1gt;lt;pgt;Nếu người dùng chọn ảnh mớilt;/pgt;lt;pregt;lt;code class=quot;language-phpquot;gt;Storage::disk(#39;public#39;)-gt;delete($post-gt;image);
lt;/codegt;lt;/pregt;lt;pgt;sau đó upload lại.lt;/pgt;lt;pgt;Nếu không chọn ảnhlt;/pgt;lt;pgt;Laravel vẫn giữ ảnh cũ.lt;/pgt;lt;pgt;Đây là cách xử lý được sử dụng trong hầu hết các CMS hiện nay.lt;/pgt;lt;hr /gt;lt;h1gt;12. Xóa bài viếtlt;/h1gt;lt;pgt;Khi xóalt;/pgt;lt;pregt;lt;code class=quot;language-phpquot;gt;Storage::disk(#39;public#39;)-gt;delete($post-gt;image);
$post-gt;delete();
lt;/codegt;lt;/pregt;lt;pgt;Không nên để file ảnh bị quot;mồ côiquot; trong thư mục Storage.lt;/pgt;lt;hr /gt;lt;h1gt;13. Giao diện quản trịlt;/h1gt;lt;pgt;Danh sách nên hiển thịlt;/pgt;lt;ulgt;lt;ligt;lt;pgt;Thumbnaillt;/pgt;lt;/ligt;lt;ligt;lt;pgt;Tiêu đềlt;/pgt;lt;/ligt;lt;ligt;lt;pgt;Danh mụclt;/pgt;lt;/ligt;lt;ligt;lt;pgt;Tác giảlt;/pgt;lt;/ligt;lt;ligt;lt;pgt;Ngày tạolt;/pgt;lt;/ligt;lt;ligt;lt;pgt;Trạng tháilt;/pgt;lt;/ligt;lt;ligt;lt;pgt;Nút Editlt;/pgt;lt;/ligt;lt;ligt;lt;pgt;Nút Deletelt;/pgt;lt;/ligt;lt;/ulgt;lt;pgt;Có thể kết hợp:lt;/pgt;lt;ulgt;lt;ligt;lt;pgt;Tailwind CSSlt;/pgt;lt;/ligt;lt;ligt;lt;pgt;DataTables.netlt;/pgt;lt;/ligt;lt;ligt;lt;pgt;Badge màult;/pgt;lt;/ligt;lt;ligt;lt;pgt;Icon Heroiconslt;/pgt;lt;/ligt;lt;ligt;lt;pgt;Xác nhận trước khi xóalt;/pgt;lt;/ligt;lt;/ulgt;lt;pgt;Đây cũng là giao diện mà chúng ta sẽ sử dụng xuyên suốt các module còn lại.lt;/pgt;lt;hr /gt;lt;h1gt;14. Kết quả đạt đượclt;/h1gt;lt;pgt;Sau bài học này, Blog CMS đã có module quản lý bài viết hoàn chỉnh.lt;/pgt;lt;pgt;✔ Thêm bài viếtlt;/pgt;lt;pgt;✔ Sửa bài viếtlt;/pgt;lt;pgt;✔ Xóa bài viếtlt;/pgt;lt;pgt;✔ Upload ảnhlt;/pgt;lt;pgt;✔ Relationship Userlt;/pgt;lt;pgt;✔ Relationship Categorylt;/pgt;lt;pgt;✔ Validationlt;/pgt;lt;pgt;✔ Hiển thị ảnhlt;/pgt;lt;pgt;✔ Phân tranglt;/pgt;lt;pgt;✔ Quản lý tác giảlt;/pgt;lt;pgt;Đây là một module CRUD thực tế mà bạn sẽ gặp trong hầu hết các hệ thống quản trị nội dung sử dụng Laravel.lt;/pgt;lt;hr /gt;lt;h1gt;Tổng kếtlt;/h1gt;lt;pgt;Trong bài học này chúng ta đã kết hợp rất nhiều kiến thức đã học trước đó:lt;/pgt;lt;ulgt;lt;ligt;lt;pgt;lt;stronggt;Resource Controllerlt;/stronggt; để xây dựng CRUD nhanh chóng.lt;/pgt;lt;/ligt;lt;ligt;lt;pgt;lt;stronggt;Eloquent Relationshiplt;/stronggt; để liên kết Post với Category và User.lt;/pgt;lt;/ligt;lt;ligt;lt;pgt;lt;stronggt;Validationlt;/stronggt; để đảm bảo dữ liệu hợp lệ.lt;/pgt;lt;/ligt;lt;ligt;lt;pgt;lt;stronggt;Storagelt;/stronggt; để quản lý hình ảnh.lt;/pgt;lt;/ligt;lt;ligt;lt;pgt;lt;stronggt;Authenticationlt;/stronggt; để tự động gán tác giả cho bài viết.lt;/pgt;lt;/ligt;lt;ligt;lt;pgt;lt;stronggt;Paginationlt;/stronggt; giúp danh sách bài viết dễ theo dõi khi số lượng tăng lên.lt;/pgt;lt;/ligt;lt;/ulgt;lt;pgt;Đến thời điểm này, dự án Blog CMS đã gần hoàn chỉnh và có cấu trúc tương tự nhiều hệ thống quản trị nội dung được sử dụng trong thực tế.lt;/pgt;lt;hr /gt;lt;h1gt;Bài tiếp theolt;/h1gt;lt;pgt;Ở lt;stronggt;Bài 28 — CRUD Userslt;/stronggt;, chúng ta sẽ xây dựng module quản lý người dùng dành cho quản trị viên:lt;/pgt;lt;ulgt;lt;ligt;lt;pgt;Danh sách Userlt;/pgt;lt;/ligt;lt;ligt;lt;pgt;Chỉnh sửa thông tinlt;/pgt;lt;/ligt;lt;ligt;lt;pgt;Thay đổi Role (Admin/User)lt;/pgt;lt;/ligt;lt;ligt;lt;pgt;Khóa hoặc mở khóa tài khoảnlt;/pgt;lt;/ligt;lt;ligt;lt;pgt;Đặt lại mật khẩult;/pgt;lt;/ligt;lt;/ulgt;lt;pgt;Sau bài này, Blog CMS sẽ có đầy đủ ba module quản trị cốt lõi: lt;stronggt;Categorieslt;/stronggt;, lt;stronggt;Postslt;/stronggt; và lt;stronggt;Userslt;/stronggt;.lt;/pgt;