Laravel Form Request giúp Controller dễ bảo trì hơn

Trong mô hình MVC, Controller sẽ nhận các dữ liệu từ HTTP request, kiểm tra (validate) dữ liệu trước khi thực hiện các bước tiếp theo là lưu dữ liệu xuống database thông qua Model. Trong Laravel Controller, chúng ta có thể thực hiện các đoạn mã kiểm tra dữ liệu, tuy nhiên khi các đoạn mã validate trở nên phức tạp, code trong Controller sẽ rất khó đọc, khó bảo trì. Framework Laravel từ phiên bản 5.0, Laravel ra mắt các class Form Request, nó giúp cho mã trong Controller gọn gàng hơn với việc tập trung xử lý về xác thực người dùng và kiểm tra dữ liệu.

Mô hình MVC

1. Form Request là gì?

Form Request chỉ đơn giản là các class được tạo ra phục vụ cho mục đích chứa các mã nghiệp vụ kiểm tra dữ liệu và phân quyền với các HTTP request.

1.1 Tạo Form Request bằng câu lệnh artisan

Các class Form Request có thể được tạo ra bằng câu lệnh artisan make:request.

php artisan make:request StoreBlogPost

Khi đó, một class StoreBlogPost sẽ được tạo ra trong thư mục app\Http\Requests. Chú ý, nếu thư mục này chưa tồn tại bạn cũng đừng lo vì câu lệnh artisan trên sẽ tự động tạo ra nếu chưa có.

1.2 Kiểm tra dữ liệu bằng phương thức rules

Trong class Form Request sẽ có hai phương thức được đưa vào sẵn là authorize() và rules(). Phương thức rules() được sử dụng để kiểm tra dữ liệu được nhập vào từ HTTP request, ví dụ kiểm tra như sau:

/**
 * Get the validation rules that apply to the request.
 *
 * @return array
 */
public function rules()
{
    return [
        'title' => 'required|unique:posts|max:255',
        'body' => 'required',
        'publication_date' => 'date',
    ];
}

Chúng ta đã định nghĩa xong các rule, nhưng bằng cách nào các rule này được sử dụng để kiểm tra. Trong phương thức của Controller chúng ta sẽ sử dụng type-hint của Form Request sử dụng. Form request sẽ được sử dụng để validate trước khi phương thức trong Controller được gọi, như vậy code trong Controller không còn lộn xộn với các logic kiểm tra nữa:

/**
 * Store the incoming blog post.
 *
 * @param  StoreBlogPost  $request
 * @return Response
 */
public function store(StoreBlogPost $request)
{
    // The incoming request is valid...
}

Nếu kiểm tra dữ liệu không qua được, một response chuyển hướng được sinh ra và gửi ngược trở lại cho người dùng cùng với các thông báo lỗi. Nếu request ở dạng AJAX thì một HTTP response với mã trạng thái 422 sẽ được trả về cùng với dữ liệu dạng JSON chứa các lỗi kiểm tra.

1.3 Xử lý công việc sau khi kiểm tra dữ liệu

Trong tình huống bạn muốn có những xử lý logic sau khi kiểm tra dữ liệu được thực hiện, bạn có thể sử dụng hook after trong Form Request với phương thức withValidator. Phương thức này nhận constructed validator cho phép bạn gọi bất kỳ phương thức nào của nó trước khi các validation rule được sử dụng.

/**
 * Configure the validator instance.
 *
 * @param  \Illuminate\Validation\Validator  $validator
 * @return void
 */
public function withValidator($validator)
{
    $validator->after(function ($validator) {
        if ($this->somethingElseIsInvalid()) {
            $validator->errors()->add('field', 'Something is wrong with this field!');
        }
    });
}

1.4 Mã logic phân quyền trong Form Request

Các class Form Request cũng chứa một phương thức authorize, trong phương thức này chúng ta có thể kiểm tra một người dùng được xác thực có thẩm quyền thực hiện công việc của Controller này không? Ví dụ, bạn muốn xác định một người dùng có quyền cập nhật bình luận trong bài viết của họ không?

/**
 * Determine if the user is authorized to make this request.
 *
 * @return bool
 */
public function authorize()
{
    $comment = Comment::find($this->route('comment'));

    return $comment && $this->user()->can('update', $comment);
}

Nếu phương thức authorize trả về false, một HTTP request với mã trạng thái 403 sẽ được tự động trả về và phương thức trong Controller sẽ không được thực hiện. Nếu bạn có kế hoạch thực hiện các logic phân quyền trong một phần khác của ứng dụng, bạn chỉ cần trả về true trong phương thức authorize:

/**
 * Determine if the user is authorized to make this request.
 *
 * @return bool
 */
public function authorize()
{
    return true;
}

1.5 Quản lý lỗi truy nhập vào tài nguyên không được phân quyền

Phương thức forbiddenResponse trong class Form Request được sử dụng để xử lý khi một người dùng truy cập vào một tài nguyên không được phân quyền. Ví dụ, một người dùng không thể sửa comment không thuộc về mình, ví dụ comment này có id là 100 thì khi truy nhập vào http://allaravel.dev/comment/100 nó sẽ sinh ra một exception, chúng ta muốn hiển thị rằng bình luận này thuộc về người dùng khác và thông báo người dùng không có quyền thực hiện bằng cách sử dụng phương thức forbiddenResponse().

public function forbiddenResponse()
{
    return response()->view('errors.403');
}

Bạn cần tạo ra một view trong thư mục resources\views\errors có tên là 403.blade.php, như vậy mỗi khi thực hiện một phương thức của Controller có sử dụng Form Request này, nếu người dùng không được cấp phép thực hiện nó sẽ hiển thị view 403 này.

Tương tự nếu bạn muốn xử lý khi kiểm tra dữ liệu không qua được, bạn có thể dùng phương thức response().

1.6 Xây dựng validator riêng

Laravel đã xây dựng sẵn bên trong rất nhiều các phương thức kiểm tra, chúng ta có phương thức validator() có thể sử dụng để điều chỉnh mã kiểm tra.

<?php
...
class FriendFormRequest extends FormRequest
{
    public function validator(ValidationService $service)
    {
        $validator = $service->getValidator($this->input());

        // Optionally customize this version using new ->after()
        $validator->after(function() use ($validator) {
            // Do more validation

            $validator->errors()->add('field', 'new error');
        });
    }
}

1.7 Một số các thuộc tính trong Form Request

Trong các class Form Request có một số thuộc tính bạn có thể khai báo với các mục đích khác nhau:

  • $redirect: đường dẫn sẽ chuyển hướng đến nếu kiểm tra dữ liệu không qua được.
  • $redirectRoute: route sẽ chuyển hướng đến nếu kiểm tra dữ liệu không qua được.
  • $redirectAction: controller action sẽ chuyển hướng đến nếu kiểm tra dữ liệu không qua được.
  • $dontFlash: Các dữ liệu nhập trong các trường bạn không muốn flash trực tiếp (mặc định: [‘password’, ‘password_confirmation’])a

2. Thực hành với Form Request

Chúng ta cùng xem xét một ví dụ sử dụng Form Request để xử lý các công việc kiểm tra dữ liệu theo cách riêng. Trong ví dụ này chúng ta quản lý việc tạo ra các bài viết trong một website, bài viết có tiêu đề, nội dung và ngày đăng bài. Theo xử lý thông thường, controller sẽ truy nhập vào các tham số của HTTP request, kiểm tra chúng và dựa vào kết quả đó để quyết định các hành động tiếp theo như thông báo lỗi nếu không vượt qua được kiểm tra dữ liệu hoặc tạo ra bài viết trong database nếu việc kiểm tra dữ liệu hoàn thành. Trong ví dụ này, chúng ta sẽ thêm vào một quy tắc kiểm tra dữ liệu riêng là tiêu đề và nội dung bài viết phải không chứa các từ ngữ vi phạm thuần phong mỹ tục.

Đầu tiên chúng ta tạo ra một class Form Request với câu lệnh artisan:

php artisan make:request CreatePostFormRequest

Câu lệnh này sẽ tạo ra class CreatePostFormRequest.php trong thư mục app\Http\Requests

<?php

namespace App\Http\Requests;

use Illuminate\Foundation\Http\FormRequest;

class CreatePostFormRequest extends FormRequest
{
    public function authorize()
    {
        return true;
    }

    public function rules()
    {
        return [
            'title' => 'required|alpha_num|max:70',
            'body' => 'required|alpha_num',
            'public_date' => 'required|date'
        ];
    }
}

Trên đây là các quy tắc cơ bản để kiểm tra dữ liệu nhập vào, với yêu cầu kiểm tra xem các nội dung không chứa các từ ngữ vi phạm thuần phong mỹ tục chúng ta sẽ xem xét ở phần tiếp theo. Để sử dụng class Form Request này chúng ta chỉ đơn giản sử dụng type-hint trong tham số của controller:

<?php

namespace App\Http\Controllers;

use App\Http\Requests\CreatePostFormRequest;

class PostController extends Controller
{
    public function store(CreatePostFormRequest $request)
    {
        // ...
    }
}

Như vậy các request đến route post.store sẽ được kiểm tra với các quy tắc được định nghĩa trong CreatePostFormRequest với tiêu đề là dạng chuỗi số hoặc chữ dài tối đa 70 ký tự, nội dung là bắt buộc và có dạng chữ hoặc số, ngày đăng cũng bắt buộc và có dạng dữ liệu ngày tháng. Các request được kiểm tra sai sẽ tự động chuyển hướng ngược lại trang nhập liệu với lỗi cụ thể. Như vậy code trong controller có thể tập trung vào các kịch bản nhập liệu.

Framework Laravel đã tạo sẵn rất nhiều các phương thức kiểm tra, nhưng trong những tình huống cụ thể có thể chúng ta cần những cách thức kiểm tra đặc biệt mang tính cá nhân. Việc mở rộng rất đơn giản bằng cách đưa vào phương thức khởi tạo trong các class Form Request. Phần tiếp theo chúng ta sẽ thực hiện kiểm tra xem nội dung của tiêu đề có chứa các từ ngữ bị cấm hay không?

<?php

namespace App\Http\Requests;

use Illuminate\Foundation\Http\FormRequest;
use Illuminate\Validation\Factory;

class CreatePostFormRequest extends FormRequest
{
    // dirty_text là mảng chứa các từ không phù hợp, chỉ mang tính ví dụ.
    protected $dirty_text = ['vcl', 'sml'];
    public function __construct(Factory $factory)
    {
        $name = 'is_clean_text';
        $test = function ($_, $value, $_) {
            $str = preg_replace('/\s+/', ' ', $value);
            $word_arr = explode(" ", $str);
            foreach($word_arr as $word){
                if(isset($this->dirty_text[$word])){
                    return true;
                }
            }
            return false;
        };
        $errorMessage = 'Nội dung không được chứa các từ ngữ vi phạm thuần phong mỹ tục.';

        $factory->extend($name, $test, $errorMessage);
    }

    public function authorize()
    {
        return true;
    }

    public function rules()
    {
        return [
            'title' => 'required|alpha_num|max:70|is_clean_text',
            'body' => 'required|alpha_num',
            'public_date' => 'required|date',
        ];
    }
}

Như vậy chúng ta đã thực hiện được một quy tắc kiểm tra riêng, tuy nhiên cách thức đưa vào phương thức khởi tạo như vậy sẽ rất khó để sử dụng lại cũng như rất khó khăn khi thực hiện unit testing. Chúng ta cần tạo ra các lớp Validator để có thể sử dụng lại. Trước hết chúng ta tạo một interface CustomValidator để các lớp validator cá nhân có thể thực hiện từ interface này.

<?php

namespace App\Http\Validations;

interface CustomValidation
{
    public function name();
    public function test();
    public function errorMessage();
}

Khi đó chúng ta tạo ra một class validator với quy tắc lọc các từ khóa vi phạm thuần phong mỹ tục.

<?php

namespace App\Http\Validations;

use CustomValidation;

class CleanText implements CustomValidation
{
    public function name()
    {
        return 'is_clean_text';
    }

    public function test()
    {
        return function ($_, $value, $_) {
            $str = preg_replace('/\s+/', ' ', $value);
            $word_arr = explode(" ", $str);
            foreach($word_arr as $word){
                if(isset($this->dirty_text[$word])){
                    return true;
                }
            }
            return false;
        }
    }

    public function errorMessage()
    {
        return 'Nội dung không được chứa các từ ngữ vi phạm thuần phong mỹ tục.';
    }
}

Như vậy khi cần sử dụng quy tắc is_clean_text chúng ta chỉ cần đưa vào như sau:

<?php

namespace App\Http\Requests;

use App\Http\Validations\CleanText;
use Illuminate\Foundation\Http\FormRequest;
use Illuminate\Validation\Factory;

class CreatePostFormRequest extends FormRequest
{
    public function __construct(Factory $factory)
    {
        $this->useCustomValidations($factory, $this->applicableValidations());
    }

    public function authorize()
    {
        return true;
    }

    public function rules()
    {
        return [
            'title' => 'required|alpha_num|max:70|is_clean_text',
            'body' => 'required|alpha_num|is_clean_text',
            'public_date' => 'required|date',
        ];
    }

    private function applicableValidations()
    {
        return collect([
            new CleanText(),
            // thêm các class Validator khác vào đây
        ]);
    }

    private function useCustomValidations($factory, $validations)
    {
        $validations->each(function ($validation) use ($factory) {
            $factory->extend($validation->name(), $validation->test(), $validation->errorMessage());
        });
    }
}

3. Lời kết

Framework Laravel luôn làm cho chúng ta bất ngờ với các thành phần sáng tạo thêm, Form Request cũng vậy, nó giúp chúng ta tách biệt phần kiểm tra dữ liệu và các logic trong xác thực người dùng (sử dụng trong phân quyền) sang một thành phần mới, giúp cho mã nguồn trong Controller trở lên rõ ràng hơn. Bài viết chưa có ví dụ về việc sử dụng các class Form Request trong các tình huống xác thực quyền người dùng, trong các bài viết tiếp theo, chúng ta sẽ còn quay trở lại vấn đề này.

6 thoughts on “Laravel Form Request giúp Controller dễ bảo trì hơn

    1. Mình chưa sử dụng Laravel Spark vì các dự án của mình chưa cái nào đụng đến SaaS, đây cũng là một chủ đề mà đã có trong dự kiến sẽ có một vài bài viết trong thời gian tới.
      Laravel Spark là sản phẩm rất tuyệt vời của Taylor Otwell, nó là bộ khung cho các dịch vụ kiểu SaaS (là các ứng dụng kiểu dịch vụ mà bạn sẽ không cần quan tâm đến cơ sở hạ tầng, ví dụ các dịch vụ của Amazon). Laravel Spark không miễn phí, bạn có thể sử dụng các bản free nhưng nó phát triển trên phiên bản Laravel tương đối cũ 5.2 thì phải. Vài nét sơ lược trước cho những bạn chưa biết Laravel Spark là gì.

    2. Mình dùng Laravel Spark rồi, nó là một mớ hỗn độn, rất nhiều file bị duplicate lên nên việc tìm file trong source sẽ tốn gấp 3 lần trí tuệ 😀 Tuy nhiên về độ nhanh thì nó sử dụng Vuejs nên có thể nói là bước đột phá trong việc muốn triển khai hệ thống một cách nhanh chóng.

  1. Ad viết thêm về các hình thức thanh toán trực tuyến cho người VN sử dụng đi, mình chưa tìm thấy tut nào trên mạng về phần này cả. Laravel hỗ trợ Stripe và Braintree mà nó có dùng được ở VN đâu, mấy cái này hỗ trợ plan sub khá hay 🙁

  2. ad cho mình hỏi
    trong trường hợp edit hoạc thêm dữ liệu , và mình muốn kiểm tra là dữ liệu đấy đã tồn tại hay chưa mới dc update vào database thì làm kiểu gì .
    trong trường hợp mình viết trực tiếp trong controller thì như này là ok
    ‘cate_name’ => ‘required|unique:nfa_categories,category_name,’.$id.’,cat_id|min:3|max:100′,

    còn trong trường hợp dùng file request như trên thì làm thế nào để check trường hợp dữ liệu đã tồn tại ạ

Add Comment