Nguyễn Đình Cường
0 comment
09 Nov, 2026
Sau khi cài được Laravel, bước tự nhiên tiếp theo là xây một ứng dụng web có giao diện. Bài này hướng dẫn bạn làm blog nhỏ gồm trang danh sách bài viết, trang chi tiết và form tạo bài, qua đó hiểu mô hình MVC của Laravel.
Khi người dùng mở một địa chỉ, route quyết định controller nào xử lý. Controller lấy dữ liệu thông qua model (Eloquent), rồi truyền sang view (Blade) để vẽ HTML. Nắm vững chuỗi route, controller, model, view là nắm được xương sống của Laravel.
php artisan make:model Post -mcLệnh tạo model Post, file migration và controller. Mở file migration và khai báo cột:
public function up(): void
{
Schema::create('posts', function (Blueprint $table) {
$table->id();
$table->string('title');
$table->string('slug')->unique();
$table->text('body');
$table->timestamps();
});
}Trong app/Models/Post.php:
class Post extends Model
{
protected $fillable = ['title', 'slug', 'body'];
public function getRouteKeyName(): string
{
return 'slug';
}
}Hàm getRouteKeyName khiến Laravel tìm bài theo slug thay vì id, nên địa chỉ có dạng thân thiện như /bai-viet/hoc-laravel. Chạy php artisan migrate để tạo bảng.
use App\Http\Controllers\PostController;
Route::get('/', [PostController::class, 'index'])->name('posts.index');
Route::get('/bai-viet/tao-moi', [PostController::class, 'create'])->name('posts.create');
Route::post('/bai-viet', [PostController::class, 'store'])->name('posts.store');
Route::get('/bai-viet/{post}', [PostController::class, 'show'])->name('posts.show');Đặt route tao-moi trước route có tham số {post}, nếu không Laravel sẽ hiểu "tao-moi" là một slug. Việc đặt tên route bằng name() cho phép tạo đường dẫn bằng hàm route('posts.show', $post) mà không viết cứng.
use App\Models\Post;
use Illuminate\Http\Request;
use Illuminate\Support\Str;
class PostController extends Controller
{
public function index()
{
$posts = Post::latest()->paginate(10);
return view('posts.index', compact('posts'));
}
public function create()
{
return view('posts.create');
}
public function store(Request $request)
{
$data = $request->validate([
'title' => 'required|string|max:200',
'body' => 'required|string',
]);
$data['slug'] = Str::slug($data['title']) . '-' . Str::lower(Str::random(4));
$post = Post::create($data);
return redirect()->route('posts.show', $post)
->with('status', 'Đã đăng bài viết!');
}
public function show(Post $post)
{
return view('posts.show', compact('post'));
}
}Hàm Str::slug chuyển tiêu đề thành chuỗi không dấu, nối bằng gạch ngang. Ta thêm vài ký tự ngẫu nhiên để tránh trùng slug khi hai bài có cùng tiêu đề. Hàm paginate(10) tự lo phân trang.
Tạo layout chung resources/views/layouts/app.blade.php:
<!DOCTYPE html>
<html lang="vi">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>@yield('title', 'Blog của tôi')</title>
</head>
<body>
<header><a href="{{ route('posts.index') }}">Blog của tôi</a></header>
<main>
@if (session('status'))
<p>{{ session('status') }}</p>
@endif
@yield('content')
</main>
</body>
</html>View danh sách resources/views/posts/index.blade.php:
@extends('layouts.app')
@section('content')
<h1>Bài viết mới</h1>
@forelse ($posts as $post)
<article>
<h2><a href="{{ route('posts.show', $post) }}">{{ $post->title }}</a></h2>
<p>{{ Str::limit($post->body, 120) }}</p>
</article>
@empty
<p>Chưa có bài viết nào.</p>
@endforelse
{{ $posts->links() }}
@endsectionView form tạo bài posts/create.blade.php. Chú ý chỉ thị @csrf bắt buộc cho form POST để chống tấn công CSRF:
@extends('layouts.app')
@section('content')
<form method="POST" action="{{ route('posts.store') }}">
@csrf
<input name="title" value="{{ old('title') }}" placeholder="Tiêu đề">
@error('title') <small>{{ $message }}</small> @enderror
<textarea name="body" rows="8">{{ old('body') }}</textarea>
@error('body') <small>{{ $message }}</small> @enderror
<button type="submit">Đăng bài</button>
</form>
@endsectionHàm old() giữ lại dữ liệu đã nhập khi validation thất bại, còn @error hiển thị thông báo lỗi theo từng trường. Nội dung trong {{ }} luôn được escape tự động.
Khi gặp lỗi, đọc kỹ trang báo lỗi của Laravel ở chế độ debug: nó chỉ đúng file, đúng dòng và gợi ý nguyên nhân. Dùng php artisan route:list để kiểm tra route, php artisan tinker để thử truy vấn Eloquent ngay trong terminal. Nhớ đặt APP_DEBUG=false khi đưa lên môi trường thật để không lộ thông tin nhạy cảm. Và hãy tập thói quen chạy php artisan migrate:fresh --seed trong môi trường phát triển để thử lại từ đầu với dữ liệu mẫu, thay vì sửa tay trong cơ sở dữ liệu.
Bạn đã đi qua toàn bộ vòng đời của một tính năng trong Laravel: migration, model, route, controller và view. Từ đây thêm tính năng mới chỉ là lặp lại và biến tấu mẫu này.
Nếu bạn muốn học Laravel một cách có hệ thống và làm dự án thực tế, hãy xem các khóa học tại codebem.com.
Nguyễn Đình Cường
0 comment