[Laravel] Hướng dẫn cách Query Filter đơn giản trong Laravel

Đăng bởi: Admin | Lượt xem: 4469 | Chuyên mục: Laravel

Query filter là một vấn đề khá quen thuộc cho dù bạn code ngôn ngữ hay nền tảng nào. Các form search dữ liệu nâng cao với field, column cần filter là chức năng thông thường của website. Nhưng dường như để làm nó một cách rõ ràng, dễ dàng mở rộng và tái sử dụng thì không đơn giản. Trong bài viết này, mình sẽ hướng dẫn query filter cho laravel. Bạn sẽ thấy rằng, nó không khó quá đâu.


Cách làm thông thường

public function index(Request $request)
{
    $user = User::query();

    if ($request->has('name')) {
        $user->where('name', 'LIKE', '%' . $request->name . '%');
    }
    if ($request->has('status')) {
        $user->where('status', $request->status);
    }
    if ($request->has('birthday')) {
        $user->whereDate('birthday', $request->birthday);
    }

    return $user->get();
}

Phía trên là code với cách làm thông thường, rất dễ để viết, người đọc cũng không khó khăn để hiểu. Nhưng nhược điểm của cách này rất nhiều: khó kiểm soát nếu có nhiều field, khó tái sử dụng, phải lặp đi lặp lại việc kiểm tra điều kiện if... Ta sẽ đi giải quyết những vấn đề đó.

Trước hết là việc khó tái sử dụng, ta sẽ sử dụng local scopes. Xây dựng các hàm scope trong model User

public function scopeName($query, $request)
{
    if ($request->has('name')) {
        $query->where('name', 'LIKE', '%' . $request->name . '%');
    }

    return $query;
}

public function scopeStatus($query, $request)
{
    if ($request->has('status')) {
        $query->where('status', $request->status);
    }

    return $query;
}

public function scopeBirthday($query, $request)
{
    if ($request->has('birthday')) {
        $query->whereDate('birthday', $request->birthday);
    }

    return $query;
}
Khi đó việc filter đã trở nên dễ nhìn hơn (dù chưa clean)

public function index(Request $request)
{
    $user = User::query()
        ->name($request)
        ->status($request)
        ->birthday($request);

    return $user->get();
}

Giả sử chúng ta muốn filter thêm field gender thì sao? Tất nhiên là thêm hàm scope vào Model

public function scopeGender($query, $request)
{
    if ($request->has('gender')) {
        $query->where('gender', $request->gender);
    }

    return $query;
}

rồi sử dụng nó để filter

public function index(Request $request)
{
    $user = User::query()
        ->name($request)
        ->status($request)
        ->birthday($request)
        ->gender($request);

    return $user->get();
}

Nhưng mà nếu việc filter đó dùng nhiều nơi thì phải đi tìm và thêm filter gender​ đó vào sao? Ơ... ơ... thì... chịu khó tìm và thêm vào thôi +_+

Hàm scope filter

Để tránh việc mỗi lần filter một field mới là phải đi "lùng sục" và thêm code ở khắp nơi, ta sẽ viết một hàm scope filter chịu trách nhiệm gọi các hàm filter field một cách tự động tùy vào tham số truyền vào. Ý tưởng là User sẽ được sử dụng như sau

public function index(Request $request)
{
    $user = User::filter($request);

    return $user->get();
}

Hàm filter không nên khai báo nhiều lần, nó chỉ là hàm "trung chuyển" gọi các hàm filter của mỗi model. Ta sẽ triển khai nó bằng trait để các model có thể dùng chung.

trait Filterable
{
    public function scopeFilter($query, $request)
    {
        //
    }
}

đồng thời use Filterable User

class User extends Authenticatable
{
    use Filterable;
    ...
}

Giờ ta sẽ tìm cách viết hàm filter sao cho nó có thể tự động gọi các hàm filter field của chính model đó. Bắt đầu bằng việc duyệt các tham số mà request truyền vào

public function scopeFilter($query, $request)
{
    $param = $request->all();
    foreach ($param as $field => $value) {
        //
    }

    return $query;
}

Cấu trúc của param bây giờ là

array:4 [
  "status" => "1"
  "gender" => "0"
  "name" => "reishou"
  "birthday" => "2000-01-01"
]

Ở đây lưu ý là không thể dùng các hàm scope field đã có, trong User ta sẽ đổi tên các hàm scopeNamescopeStatus... thành filterNamefilterStatus... cũng như thay đổi chút nội dung.

public function filterName($query, $value)
{
    return $query->where('name', 'LIKE', '%' . $value . '%');
}

public function filterStatus($query, $value)
{
    return $query->where('status', $value);
}

public function filterBirthday($query, $value)
{
    return $query->whereDate('birthday', $value);
}

public function filterGender($query, $value)
{
    return $query->where('gender', $value);
}

Quay lại Filterable, thêm nội dung cho hàm filter

public function scopeFilter($query, $request)
{
    $param = $request->all();
    foreach ($param as $field => $value) {
        $method = 'filter' . Str::studly($field);

        if (method_exists($this, $method)) {
            $this->{$method}($query, $value);
        }
    }

    return $query;
}

Vòng lặp sẽ duyệt từng phần tử của $param, dựa theo $field để gọi hàm tương ứng bên User. Biến $param chỉ chứa các $field có trong $request nên không cần phải sử dụng $request->has('name') nữa. Nhưng vẫn còn một chú ý nho nhỏ là cần loại trừ trường hợp $value là chuỗi rỗng ''

public function scopeFilter($query, $request)
{
    $param = $request->all();
    foreach ($param as $field => $value) {
        $method = 'filter' . Str::studly($field);

        if ($value != '') {
            if (method_exists($this, $method)) {
                $this->{$method}($query, $value);
            }
        }
    }

    return $query;
}

Đến đây có thể gọi là tạm đủ cho chức năng filter, nhưng liệu còn có thể làm thêm gì đó không?

Phải lặp lại nhiều lần? Hãy refactor nó.

Nếu nhìn lại các hàm filter field ở User, ta sẽ thấy có những hàm kiểu như sau

public function filterStatus($query, $value)
{
    return $query->where('status', $value);
}

public function filterGender($query, $value)
{
    return $query->where('gender', $value);
}

nó có điểm chung là đơn giản, chỉ "where" field bằng giá trị truyền vào. Ý tưởng là hàm filter sẽ thực hiện query tự động theo field luôn, không cần khai báo hàm filter ở model nữa. Nhưng model lại cần có cái gì đó để hàm filter nhận biết những field đó.

Tạo biến $filterable ở User

protected $filterable = [
    'status',
    'gender'
];

Bổ sung thêm code cho hàm filter

public function scopeFilter($query, $request)
{
    $param = $request->all();
    foreach ($param as $field => $value) {
        $method = 'filter' . Str::studly($field);

        if ($value != '') {
            if (method_exists($this, $method)) {
                $this->{$method}($query, $value);
            } else {
                if (!empty($this->filterable) && is_array($this->filterable)) {
                    if (in_array($field, $this->filterable)) {
                        $query->where($this->table . '.' . $field, $value);
                    }
                }
            }
        }
    }

    return $query;
}

Bởi vì không chắc model có khai báo biến $filterable nên ta cần check điều kiện trước khi thực hiện query. Tuy nhiên đến đây ta nhận ra rằng có thể có trường hợp $field truyền vào không giống với field trong database. Ví dụ trong bảng user không có column gender mà là sex chẳng hạn, ta sẽ thay đổi một chút trong User

protected $filterable = [
    'status',
    'gender' => 'sex'
];

Cập nhật thêm cho hàm filter

public function scopeFilter($query, $request)
{
    $param = $request->all();
    foreach ($param as $field => $value) {
        $method = 'filter' . Str::studly($field);

        if ($value != '') {
            if (method_exists($this, $method)) {
                $this->{$method}($query, $value);
            } else {
                if (!empty($this->filterable) && is_array($this->filterable)) {
                    if (in_array($field, $this->filterable)) {
                        $query->where($this->table . '.' . $field, $value);
                    } elseif (key_exists($field, $this->filterable)) {
                        $query->where($this->table . '.' 
                            . $this->filterable[$field], $value);
                    }
                }
            }
        }
    }

    return $query;
}

Clean code

Ta sẽ làm cho hàm filter nhìn ổn hơn, "đẹp" hơn bằng cách loại trừ biến $request, không còn lệ thuộc vào nó nữa, chỉ truyền vào mảng $param.

public function index(Request $request)
{
    $param = $request->all();
    $user = User::filter($param);

    return $user->get();
}

Ngoài ra vận dụng thêm Early Return với hàm filter

public function scopeFilter($query, $param)
{
    foreach ($param as $field => $value) {
        $method = 'filter' . Str::studly($field);

        if ($value === '') {
            continue;
        }

        if (method_exists($this, $method)) {
            $this->{$method}($query, $value);
            continue;
        }

        if (empty($this->filterable) || !is_array($this->filterable)) {
            continue;
        }

        if (in_array($field, $this->filterable)) {
            $query->where($this->table . '.' . $field, $value);
            continue;
        }

        if (key_exists($field, $this->filterable)) {
            $query->where($this->table . '.' . $this->filterable[$field], $value);
            continue;
        }
    }

    return $query;
}

Sử dụng

Như vậy là chúng ta đã triển khai xong một "cơ chế" filter đơn giản. Từ bây giờ, nếu muốn filter cho một model nào, chỉ cần use Filterable, các field đơn giản thì khai báo trong biến ​$filterable, với các logic phức tạp hơn thì tạo các hàm filter{$field}() tương ứng. Sau đó chỉ cần đơn giản gọi hàm scope filter($param) là xong.

Đánh giá

Ưu điểm:

Cách dùng gọn gàng, dễ tái sử dụng, dễ mở rộng thêm các field mới. Có thể sử dụng hàm filter chung với các hàm scope khác cũng như các hàm gốc của model theo syntax chaining method.

$user = User::filter($param)
    ->popular()
    ->vipMember();

Nhược điểm:

Khó nắm được chính xác thứ gì sẽ được filter, phải kiểm soát kỹ đầu vào trước khi gọi hàm filter. Không dễ tiếp cận đối với fresher.

vncoder logo

Theo dõi VnCoder trên Facebook, để cập nhật những bài viết, tin tức và khoá học mới nhất!



Khóa học liên quan

Khóa học: Laravel

Xây dựng ứng dụng với Laravel và Vuejs
Số bài học:
Lượt xem: 16287
Đăng bởi: Admin
Chuyên mục: Laravel

Học lập trình Laravel
Số bài học:
Lượt xem: 23327
Đăng bởi: Admin
Chuyên mục: Laravel