Route và Controller trong Laravel 13
Bài này giải thích hai khái niệm cơ bản nhất là Route và Controller, mỗi phần đều có ví dụ nhỏ để bạn chạy thử ngay.
1. Route và Controller là gì?
Bạn có thể hình dung một nhà hàng như sau:
- Route giống người lễ tân. Khách hỏi "tôi muốn vào bàn số 5", lễ tân chỉ đường cho khách đến đúng chỗ.
- Controller giống đầu bếp. Đầu bếp nhận yêu cầu, chế biến rồi trả món ăn (một trang HTML hoặc dữ liệu JSON) cho khách.
Luồng đi của một request:
Trình duyệt ──► Route (routes/web.php) ──► Controller ──► View / JSON ──► Trình duyệt
2. Route đầu tiên
Mọi route của trang web nằm trong file routes/web.php. Mở file này ra và thêm:
<?php
use Illuminate\Support\Facades\Route;
Route::get('/xin-chao', function () {
return 'Xin chào Laravel 13!';
});
Khởi động server:
php artisan serve
Mở http://127.0.0.1:8000/xin-chao và bạn sẽ thấy dòng chữ Xin chào Laravel 13!
Giải thích:
| Thành phần | Ý nghĩa |
|---|---|
Route::get |
Chỉ nhận request có phương thức GET (mở link, gõ URL) |
'/xin-chao' |
Đường dẫn (URL) |
function () {...} |
Đoạn code chạy khi có người truy cập (gọi là closure) |
Các phương thức HTTP thường gặp
Route::get('/bai-viet', ...); // Xem danh sách
Route::post('/bai-viet', ...); // Tạo mới (submit form)
Route::put('/bai-viet/{id}', ...); // Cập nhật toàn bộ
Route::patch('/bai-viet/{id}', ...); // Cập nhật một phần
Route::delete('/bai-viet/{id}', ...); // Xoá
Route trả về view hoặc chuyển hướng
Nếu route chỉ cần hiển thị một trang tĩnh, bạn không cần viết closure:
Route::view('/gioi-thieu', 'about'); // hiển thị resources/views/about.blade.php
Route::redirect('/trang-cu', '/gioi-thieu'); // chuyển hướng /trang-cu sang /gioi-thieu
3. Tham số trên route
Nhiều khi URL chứa dữ liệu, ví dụ /bai-viet/5 thì 5 là id bài viết. Bạn lấy giá trị
này bằng cách đặt tên tham số trong cặp ngoặc {}:
Route::get('/bai-viet/{id}', function (string $id) {
return "Bạn đang xem bài viết số: $id";
});
/bai-viet/5 sẽ hiển thị Bạn đang xem bài viết số: 5
Tham số không bắt buộc
Thêm dấu ? sau tên tham số và đặt giá trị mặc định cho nó:
Route::get('/chao/{ten?}', function (string $ten = 'bạn') {
return "Chào $ten!";
});
/chaohiển thị Chào bạn!/chao/Vihiển thị Chào Vi!
Giới hạn kiểu tham số
Nếu muốn id chỉ nhận số, dùng whereNumber:
Route::get('/bai-viet/{id}', function (string $id) {
return "Bài viết số $id";
})->whereNumber('id');
Khi đó /bai-viet/abc sẽ trả về lỗi 404, vì abc không phải là số.
4. Đặt tên cho route
Khi đặt tên cho route, bạn không cần gõ cứng URL ở nhiều nơi. Sau này nếu đổi URL, bạn chỉ phải sửa một chỗ.
Route::get('/lien-he', function () {
return 'Trang liên hệ';
})->name('contact');
Tạo link từ tên route:
route('contact'); // http://127.0.0.1:8000/lien-he
route('posts.show', ['id' => 5]); // dùng cho route có tham số
Trong file Blade:
<a href="{{ route('contact') }}">Liên hệ</a>
5. Controller: tách code ra khỏi file route
Viết hết code vào closure trong routes/web.php sẽ làm file rất dài và khó đọc. Vì vậy
ta chuyển phần xử lý sang Controller, còn file route chỉ làm việc "chỉ đường".
Bước 1: Tạo controller bằng Artisan
php artisan make:controller PostController
Laravel tạo file app/Http/Controllers/PostController.php:
<?php
namespace App\Http\Controllers;
class PostController extends Controller
{
//
}
Bước 2: Viết các phương thức (method)
Để không cần database, ta dùng tạm một mảng làm dữ liệu:
<?php
namespace App\Http\Controllers;
class PostController extends Controller
{
// Dữ liệu giả lập, sau này sẽ thay bằng Model + database
private array $posts = [
1 => ['title' => 'Học Laravel từ đầu', 'content' => 'Laravel là framework PHP phổ biến nhất.'],
2 => ['title' => 'Route là gì?', 'content' => 'Route giúp định tuyến URL tới đúng chỗ xử lý.'],
3 => ['title' => 'Controller là gì?', 'content' => 'Controller chứa logic xử lý request.'],
];
// GET /bai-viet: danh sách bài viết
public function index()
{
return view('posts.index', ['posts' => $this->posts]);
}
// GET /bai-viet/{id}: chi tiết một bài viết
public function show(int $id)
{
// Không tìm thấy bài viết thì trả về trang 404
abort_unless(isset($this->posts[$id]), 404);
return view('posts.show', [
'id' => $id,
'post' => $this->posts[$id],
]);
}
}
Bước 3: Nối route với controller
Trong routes/web.php:
<?php
use App\Http\Controllers\PostController;
use Illuminate\Support\Facades\Route;
Route::get('/bai-viet', [PostController::class, 'index'])->name('posts.index');
Route::get('/bai-viet/{id}', [PostController::class, 'show'])
->whereNumber('id')
->name('posts.show');
Cú pháp [PostController::class, 'index'] nghĩa là: khi có request vào URL này, gọi
method index của PostController.
Bước 4: Tạo view
resources/views/posts/index.blade.php
<!DOCTYPE html>
<html lang="vi">
<head>
<meta charset="UTF-8">
<title>Danh sách bài viết</title>
</head>
<body>
<h1>Danh sách bài viết</h1>
<ul>
@foreach ($posts as $id => $post)
<li>
<a href="{{ route('posts.show', ['id' => $id]) }}">{{ $post['title'] }}</a>
</li>
@endforeach
</ul>
</body>
</html>
resources/views/posts/show.blade.php
<!DOCTYPE html>
<html lang="vi">
<head>
<meta charset="UTF-8">
<title>{{ $post['title'] }}</title>
</head>
<body>
<h1>{{ $post['title'] }}</h1>
<p>{{ $post['content'] }}</p>
<a href="{{ route('posts.index') }}">← Quay lại danh sách</a>
</body>
</html>
Bước 5: Chạy thử
http://127.0.0.1:8000/bai-viethiển thị danh sách 3 bài viết- Bấm vào một bài sẽ mở trang chi tiết
/bai-viet/1 /bai-viet/99trả về 404 (không có bài này)
6. Nhận dữ liệu từ form với Request
Ví dụ thêm một form tìm kiếm. Laravel tự truyền đối tượng Request vào method nếu bạn
khai báo nó làm tham số:
use Illuminate\Http\Request;
// GET /tim-kiem?tu-khoa=laravel
public function search(Request $request)
{
// Kiểm tra dữ liệu đầu vào: bắt buộc nhập, tối thiểu 2 ký tự
$data = $request->validate([
'tu-khoa' => 'required|min:2',
]);
$keyword = $data['tu-khoa'];
$results = array_filter(
$this->posts,
fn ($post) => str_contains(mb_strtolower($post['title']), mb_strtolower($keyword))
);
return view('posts.index', ['posts' => $results]);
}
Route:
Route::get('/tim-kiem', [PostController::class, 'search'])->name('posts.search');
Form trong view:
<form action="{{ route('posts.search') }}" method="GET">
<input type="text" name="tu-khoa" placeholder="Tìm bài viết...">
<button type="submit">Tìm</button>
</form>
Lưu ý: form dùng
method="POST"thì bắt buộc phải có@csrfbên trong thẻ<form>. Nếu thiếu, Laravel báo lỗi 419 Page Expired.
7. Trả về JSON
Controller không chỉ trả về view. Nếu bạn trả về một mảng, Laravel tự chuyển nó thành JSON:
public function apiIndex()
{
return $this->posts; // Laravel tự chuyển sang JSON
}
Bạn cũng có thể chỉ định mã HTTP:
return response()->json(['message' => 'Không tìm thấy'], 404);
Trong Laravel 13, file
routes/api.phpkhông có sẵn. Chạyphp artisan install:apiđể tạo file này. Route trong đó sẽ tự có tiền tố/api.
8. Resource Controller: CRUD trong một dòng
Thêm, xem, sửa, xoá (CRUD) là việc rất hay gặp, nên Laravel có sẵn một cách làm nhanh:
php artisan make:controller ProductController --resource
Lệnh này tạo controller có sẵn 7 method: index, create, store, show, edit,
update, destroy. Bạn chỉ cần một dòng route:
use App\Http\Controllers\ProductController;
Route::resource('san-pham', ProductController::class);
Laravel tự tạo đủ 7 route:
| Phương thức | URL | Method controller | Tên route |
|---|---|---|---|
| GET | /san-pham |
index |
san-pham.index |
| GET | /san-pham/create |
create |
san-pham.create |
| POST | /san-pham |
store |
san-pham.store |
| GET | /san-pham/{san_pham} |
show |
san-pham.show |
| GET | /san-pham/{san_pham}/edit |
edit |
san-pham.edit |
| PUT/PATCH | /san-pham/{san_pham} |
update |
san-pham.update |
| DELETE | /san-pham/{san_pham} |
destroy |
san-pham.destroy |
Nếu chỉ cần vài route trong số đó:
Route::resource('san-pham', ProductController::class)->only(['index', 'show']);
9. Nhóm route
Khi nhiều route có chung tiền tố URL, tiền tố tên hoặc middleware, bạn gom chúng vào một nhóm:
Route::prefix('admin') // URL bắt đầu bằng /admin
->name('admin.') // tên route bắt đầu bằng admin.
->middleware('auth') // phải đăng nhập mới vào được
->group(function () {
Route::get('/dashboard', fn () => 'Trang quản trị')->name('dashboard');
// URL: /admin/dashboard, tên route: admin.dashboard
});
Nếu nhiều route cùng dùng một controller:
Route::controller(PostController::class)->group(function () {
Route::get('/bai-viet', 'index')->name('posts.index');
Route::get('/bai-viet/{id}', 'show')->name('posts.show');
});
10. Xem tất cả route đang có
php artisan route:list
Lệnh này in ra bảng gồm phương thức, URL, tên route và controller xử lý. Bạn nên dùng nó mỗi khi thấy route "không chạy".
Lỗi hay gặp
| Lỗi | Nguyên nhân thường gặp | Cách sửa |
|---|---|---|
| 404 Not Found | Gõ sai URL, hoặc tham số không khớp điều kiện where |
Chạy php artisan route:list để kiểm tra |
| Target class [PostController] does not exist | Quên dòng use App\Http\Controllers\PostController; |
Thêm use ở đầu routes/web.php |
| 419 Page Expired | Form POST thiếu @csrf |
Thêm @csrf vào trong <form> |
| 405 Method Not Allowed | Gửi POST vào route chỉ khai báo get |
Dùng đúng Route::post / @method('PUT') |
| Route mới thêm không chạy | Route đang bị cache | Chạy php artisan route:clear |
Tóm tắt
- Route (
routes/web.php) quyết định URL nào được xử lý ở đâu. - Controller (
app/Http/Controllers) chứa code xử lý và trả về view hoặc JSON. - Tham số
{id}, điều kiệnwhereNumber, tên route->name()giúp code rõ ràng và dễ bảo trì. Route::resourcetạo CRUD nhanh.Route::prefix()->group()gom các route liên quan.- Khi gặp lỗi, chạy
php artisan route:listtrước tiên.