Form Request trong Laravel: kiểm tra dữ liệu đầu vào gọn gàng

  • author-image

    Nguyễn Đình Cường

  • blog-tag
  • blog-comment 0 comment
  • created-date 09 Nov, 2026
blog-thumbnail

Dữ liệu người dùng gửi lên không bao giờ đáng tin. Nếu bạn nhét hết đoạn kiểm tra vào controller, code sẽ phình to và lặp đi lặp lại. Form Request của Laravel gom việc xác thực vào một class riêng, giúp controller mỏng và dễ đọc.

Vì sao nên tách validation ra khỏi controller?

Controller nên chỉ làm một việc: điều phối. Khi logic kiểm tra nằm lẫn trong đó, bạn khó tái sử dụng (ví dụ cùng quy tắc cho tạo mới và cập nhật), khó đọc và khó kiểm thử. Form Request tách riêng phần này, đồng thời gom luôn việc phân quyền và thông báo lỗi tùy chỉnh.

Tạo một Form Request bằng lệnh:

php artisan make:request StorePostRequest

Viết quy tắc kiểm tra

Trong class vừa tạo có hai phương thức chính: authorize và rules.

class StorePostRequest extends FormRequest{    public function authorize(): bool    {        return $this->user() !== null;    }    public function rules(): array    {        return [            'title'       => ['required', 'string', 'max:150'],            'body'        => ['required', 'string', 'min:20'],            'category_id' => ['required', 'exists:categories,id'],            'tags'        => ['array', 'max:5'],            'tags.*'      => ['string', 'max:30'],            'slug'        => ['required', 'alpha_dash', 'unique:posts,slug'],        ];    }}

Sau đó chỉ cần khai báo kiểu tham số trong controller. Laravel tự chạy kiểm tra trước khi vào hàm; nếu sai, nó tự trả về lỗi 422 (với API) hoặc quay lại form kèm thông báo (với web).

public function store(StorePostRequest $request){    $post = Post::create($request->validated());    return response()->json($post, 201);}

Hãy dùng $request->validated() thay vì $request->all(). Phương thức đầu chỉ trả về các trường đã vượt qua quy tắc, giúp bạn tránh lỗ hổng mass assignment do người dùng gửi thêm trường lạ.

Quy tắc cập nhật và dữ liệu duy nhất

Khi cập nhật, quy tắc unique phải bỏ qua chính bản ghi đang sửa, nếu không bạn sẽ bị báo trùng với chính mình:

use Illuminate\Validation\Rule;'slug' => ['required', Rule::unique('posts', 'slug')->ignore($this->route('post'))],

Đây là lỗi mà rất nhiều người mới gặp ở chức năng sửa. Bạn cũng có thể dùng Rule::in([...]) để giới hạn giá trị trong một danh sách cho phép, hoặc Rule::requiredIf() cho các trường bắt buộc có điều kiện.

Thông báo lỗi và chuẩn hóa dữ liệu

Bạn có thể tùy chỉnh thông báo bằng tiếng Việt:

public function messages(): array{    return [        'title.required' => 'Vui lòng nhập tiêu đề.',        'title.max'      => 'Tiêu đề không được dài quá 150 ký tự.',    ];}

Nếu cần làm sạch dữ liệu trước khi kiểm tra, ví dụ cắt khoảng trắng hoặc tự sinh slug, hãy dùng prepareForValidation:

protected function prepareForValidation(): void{    $this->merge(['slug' => Str::slug($this->input('slug', $this->input('title')))]);}

Những điểm cần nhớ

  • Validation phía server là bắt buộc, kể cả khi form đã kiểm tra bằng JavaScript, vì kẻ xấu có thể gọi API trực tiếp.
  • Dùng mảng quy tắc thay vì chuỗi phân tách bằng dấu | khi quy tắc phức tạp (như regex chứa ký tự đặc biệt).
  • Kiểm tra kiểu dữ liệu và giới hạn kích thước, đặc biệt với tệp tải lên: 'avatar' => ['image', 'max:2048'] (đơn vị kilobyte).
  • Đặt tên Form Request theo hành động như StorePostRequest và UpdatePostRequest để dễ tìm.
  • Viết test cho các trường hợp sai, vì đây chính là nơi lỗi bảo mật thường lọt qua.

Kiểm thử Form Request

Quy tắc kiểm tra cũng là logic nghiệp vụ, nên cần được kiểm thử. Cách đơn giản là gọi endpoint với dữ liệu sai và khẳng định phản hồi lỗi:

public function test_title_is_required(): void{    $user = User::factory()->create();    $this->actingAs($user)        ->postJson('/api/posts', ['body' => 'Nội dung đủ dài để qua kiểm tra.'])        ->assertStatus(422)        ->assertJsonValidationErrors(['title']);}

Một bộ test nhỏ cho từng quy tắc quan trọng sẽ giúp bạn tự tin sửa đổi mà không sợ làm hỏng hành vi cũ. Hãy ưu tiên các trường nhạy cảm như email, số tiền, quyền sở hữu tài nguyên.

Phân quyền trong authorize

Phương thức authorize không chỉ để trả về true. Bạn có thể kiểm tra người dùng có quyền sửa đúng bài viết này không, dùng Policy:

public function authorize(): bool{    return $this->user()->can('update', $this->route('post'));}

Nếu trả về false, Laravel tự trả mã 403. Cách này giữ cho controller sạch và đảm bảo không ai quên bước kiểm tra quyền, vì nó nằm ngay trước khi vào hàm xử lý.

Khi nào dùng validate() trực tiếp?

Không phải lúc nào cũng cần một class riêng. Với một endpoint nhỏ chỉ có hai ba trường, gọi $request->validate([...]) ngay trong controller vẫn hoàn toàn chấp nhận được. Hãy chuyển sang Form Request khi quy tắc dài, khi cần phân quyền kèm theo, hoặc khi cùng bộ quy tắc được dùng lại ở nhiều nơi.

Kết luận

Form Request giúp code rõ ràng, tái sử dụng được và an toàn hơn. Từ giờ, mỗi khi thấy mình viết $request->validate() dài dằng dặc trong controller, hãy cân nhắc chuyển nó sang một Form Request riêng. Ứng dụng của bạn sẽ gọn gàng và dễ bảo trì hơn nhiều.

Khi cần một lộ trình học có hệ thống, bạn có thể tham khảo khóa học trên codebem.com.

author_photo
Nguyễn Đình Cường

0 comment