Lập trình · 22/09/2026

Học Laravel 13 – Bài 05: Routing, Controller và Middleware trong Laravel 13

Bài 5 — Routing, Controller và Middleware: chúng ta giữ nguyên trang TaskFlow đã xây, nhưng tách việc chọn URL, xử lý màn hình và gắn mã request thành ba trách nhiệm rõ ràng. Hãy hoàn thành bài 4 về cấu hình và logging trước, vì ví dụ tiếp tục dùng config/taskflow.php và log có điều kiện.

Routing, Controller và Middleware trong Laravel 13

Bài 5 — Routing, Controller và Middleware: chúng ta giữ nguyên trang TaskFlow đã xây, nhưng tách việc chọn URL, xử lý màn hình và gắn mã request thành ba trách nhiệm rõ ràng. Hãy hoàn thành bài 4 về cấu hình và logging trước, vì ví dụ tiếp tục dùng config/taskflow.php và log có điều kiện.

1. Vì sao cần tách closure hiện tại?

Closure của bài trước đủ tốt cho một màn hình nhỏ. Khi muốn dùng cùng cơ chế nhận diện request ở nhiều màn hình, sao chép code tạo mã vào từng route sẽ dễ bỏ sót. Trong bài này, route chỉ ánh xạ GET /taskflow tới controller; controller chuẩn bị dữ liệu cho view; middleware tạo mã và gắn nó vào response. Không phải mọi closure đều cần chuyển thành controller ngay, nhưng đây là thời điểm hợp lý để luyện cách phân chia.

GET /taskflow
  → AssignRequestId: tạo UUID, đặt request attribute
  → TaskFlowOverviewController: đọc config, ghi log, trả view
  → AssignRequestId: gắn X-Request-ID vào response
  → trình duyệt nhận HTML và header

Request attribute là dữ liệu nội bộ phía server, không phải query string do người dùng gửi. X-Request-ID chỉ giúp đối chiếu log, không phải token xác thực, quyền truy cập hay bằng chứng người gọi đáng tin cậy.

2. Tạo middleware

php artisan make:middleware AssignRequestId

Thay nội dung app/Http/Middleware/AssignRequestId.php bằng:

<?php
namespace App\Http\Middleware;

use Closure;
use Illuminate\Http\Request;
use Illuminate\Support\Str;
use Symfony\Component\HttpFoundation\Response;

class AssignRequestId
{
    public function handle(Request $request, Closure $next): Response
    {
        $id = (string) Str::uuid();
        $request->attributes->set('request_id', $id);
        $response = $next($request);
        $response->headers->set('X-Request-ID', $id);

        return $response;
    }
}

Phần trước $next chạy trên đường request đi vào; phần sau chạy khi nhận được response từ lớp bên trong. Ta luôn tạo mã mới phía server, không phản chiếu một header tùy ý của client. Không đưa IP, email hoặc token vào mã. Đây là UUID phục vụ chẩn đoán, chưa phải distributed trace lan truyền qua nhiều service.

Middleware này chỉ gắn lên route /taskflow. Vì thế không hứa mọi 404 của ứng dụng đều có header; request không khớp route có thể chưa đi qua middleware này. Nếu yêu cầu request ID trên mọi response, kể cả lỗi trước routing, cần thiết kế middleware toàn cục và cách render exception riêng. Đừng mở rộng phạm vi chỉ bằng một khẳng định trong tài liệu.

3. Tạo controller một hành động

php artisan make:controller TaskFlowOverviewController --invokable

Thay nội dung app/Http/Controllers/TaskFlowOverviewController.php:

<?php
namespace App\Http\Controllers;

use Illuminate\Contracts\View\View;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Log;

class TaskFlowOverviewController extends Controller
{
    public function __invoke(Request $request): View
    {
        if (config('taskflow.overview_logging')) {
            Log::info('taskflow.overview.viewed', [
                'route' => 'taskflow.overview',
                'request_id' => $request->attributes->get('request_id'),
            ]);
        }

        return view('taskflow.overview', [
            'projectName' => config('taskflow.name'),
            'milestone' => 'Request lifecycle',
        ]);
    }
}

__invoke cho phép dùng class controller làm một hành động. Request được Laravel cung cấp qua type hint; bạn không tự new Request hoặc đọc trực tiếp $_GET. Controller trả View, giữ nguyên dữ liệu và template của bài trước. Log nhận cùng request_id sẽ xuất hiện trong header, nhưng vẫn chỉ gồm các field được phép.

4. Nối route với controller và middleware

Nội dung routes/web.php ở mốc bài học này là:

<?php
use App\Http\Controllers\TaskFlowOverviewController;
use App\Http\Middleware\AssignRequestId;
use Illuminate\Support\Facades\Route;

Route::get('/', function () {
    return view('welcome');
});

Route::get('/taskflow', TaskFlowOverviewController::class)
    ->middleware(AssignRequestId::class)
    ->name('taskflow.overview');

Dùng class middleware trực tiếp nên chưa cần alias trong bootstrap/app.php. Khi một nhóm route cùng dùng middleware, có thể tổ chức group; đừng đăng ký đồng thời toàn cục và riêng route nếu không muốn chạy hai lần. Tên route taskflow.overview vẫn giữ nguyên nên test và liên kết dùng route() không cần đổi.

php artisan route:list --path=taskflow -vv
php artisan test

Route list phải hiển thị GET|HEAD, TaskFlowOverviewController và AssignRequestId. Đừng thêm route GET thứ hai cùng URI để "thử" controller; thay định nghĩa cũ. Với resource route trong tương lai, chú ý các path cố định như /tasks/create và tham số /tasks/{task}; ràng buộc tham số và thứ tự route cần được kiểm tra bằng test.

5. Kiểm thử mã request và cập nhật test log

Tạo tests/Feature/TaskFlowRequestIdTest.php:

<?php
namespace Tests\Feature;

use Illuminate\Support\Str;
use Tests\TestCase;

class TaskFlowRequestIdTest extends TestCase
{
    public function test_request_id_is_server_generated_and_changes_per_request(): void
    {
        $first = $this->withHeader('X-Request-ID', 'untrusted-client-value')
            ->get('/taskflow')->assertOk();
        $id = $first->headers->get('X-Request-ID');
        $this->assertTrue(Str::isUuid($id));
        $this->assertNotSame('untrusted-client-value', $id);

        $second = $this->get('/taskflow')->assertOk();
        $this->assertNotSame($id, $second->headers->get('X-Request-ID'));
    }
}

Test log của bài 4 đang kỳ vọng context chỉ có route nên cần cập nhật chủ động, không xóa test. Gán response từ lời gọi get() vào $response, rồi đổi phần kỳ vọng context thành:

['route' => 'taskflow.overview',
 'request_id' => $response->headers->get('X-Request-ID')]

Điều này kiểm tra ID trong log khớp response, trong khi test mới kiểm tra UUID được tạo bởi server và khác nhau giữa hai request. Bộ test hiện tại đã pass 8 test, 18 assertions. Hai UUID khác nhau trong test không phải chứng minh toán học rằng không bao giờ trùng; đây là kiểm tra hồi quy cho cách sử dụng cơ chế tạo mã.

6. Quan sát bằng HTTP thực tế

Chạy server local của bài 2 rồi dùng curl (PowerShell có thể gọi curl.exe):

curl -i http://127.0.0.1:8000/taskflow

Tìm X-Request-ID ở response. Nếu bật TASKFLOW_OVERVIEW_LOGGING và config không còn cache cũ, tìm cùng mã trong channel log đã chọn. Không cần công khai log để người dùng tra cứu; bộ phận vận hành có thể nhận ID qua phản hồi lỗi và tìm trong hệ thống nội bộ.

7. Bài tập và lỗi cần tránh

Tạm bỏ middleware khỏi route và dự đoán test nào thất bại, sau đó khôi phục. Đổi path sang /workspace nhưng giữ tên route rồi kiểm tra nơi nào còn hardcode URL. Không dùng request ID để authorize; quyền truy cập TaskFlow sẽ được bổ sung trong phần authentication và policy.

Nếu class không tìm thấy, so namespace với đường dẫn và import. Nếu không có header, kiểm tra route:list trước khi sửa view. Nếu log không xuất hiện, kiểm tra cờ cấu hình và level như bài 4. Controller chưa cần truy vấn database; bài này không chứng minh chức năng quản lý task đã hoàn thành.

Tài liệu đối chiếu: Routing, Controllers, Middleware. Điều hướng: Bài 4 · Lộ trình. Bài tiếp theo tổ chức giao diện bằng Blade component và Vite.

Điều hướng khóa học Laravel 13

Bài trước (04) · Bài sau (06) · Mục lục trọn bộ 37 bài

Thảo luận

Bình luận 0

Đăng nhập để bình luận

Bạn cần có tài khoản để tham gia thảo luận và trả lời độc giả khác.

Đăng nhậpĐăng ký

Chưa có bình luận. Hãy là người đầu tiên chia sẻ ý kiến.