Mẹo Query Filter Đơn Giản Trong Laravel Cho Các Bạn Mới Vào Nghề

Mẹo Query Filter Đơn Giản Trong Laravel Cho Các Bạn Mới Vào Nghề
Trong một dự án thực tế, các bạn thường phải query filter cho các phần search data ở 1 trang web là chuyện bình thường. Xây dựng filter hiệu quả và dễ sử dụng là thử thách.

Query filter là câu chuyện quen thuộc khi các bạn phải lập trình ngôn ngữ hoặc trên bất cứ nền tảng nào. Các form search dữ liệu nâng cao thường với field, column cần filter là tính năng thông thường của một website. Để thực hiện điều này sao cho rõ ràng và dễ trong vấn đề sử dụng lâu dài thì không thực sự dễ. Đó là lý do để mình bật mí cho các bạn biết làm thế nào để query filter trong Laravel. 

 

Cách query filter thông thường 

 

Các bạn xem ví dụ mẫu dưới đây:

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();
}

 

Các bạn lập trình thường sử dụng cách này vì nó dễ code. Tuy nhiên, khuyết điểm là các bạn rất khó kiểm soát. Kiểm soát cái gì? Đó là khi bạn cần tìm nhiều field. Bạn phải lặp lại thao tác gõ điều kiện if nhiều lần và còn không được tái sử dụng. Bất tiện đúng không? Vậy có lối đi nào dễ thở hơn không? Có. Các bạn nên code như thế này:

 

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;
}

 

Sau đó, chúng ta sẽ gọi như sau:

 

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

 

Các bạn có thấy nhìn thấy đỡ rối mắt hơn nhiều phải không? Không chỉ có vậy. Các bạn còn có thể tái sử dụng ở một số trường hợp, ví dụ như cần filter theo 1 field nào đó. 

 

Local scopes

 

Khi không thể tái sử dụng, các bạn sẽ thử dùng local scopes và 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;
}

 

Nếu bạn muốn filter thêm field gender, thì bạn cần bổ sung hàm scope vào model. 

 

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

 

Đừng quên filter.

 

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

    return $user->get();
}

 

Hàm scope filter 

 

Để không phải filter nhiều field mới là phải mất công và thời gian tìm kiếm khắp nơi, các bạn nên tạo 1 hàm scope filter. Hàm này có trách nhiệm gọi các hàm filter field tự động truyền vào tham số. 

 

public function index(Request $request)
{
    $user = User::filter($request);
    return $user->get();
}

 

Các bạn không nên khai báo hàm này nhiều lần. Chỉ nên triển khai nó bằng trait để các model có thể dùng chung. 

 

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

 

Thêm use Filterable ở User:

 

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

 

Cách xây dựng class filter 

 

Để tránh vấp phải trường hợp mỗi lần cần filter thì ta lại phải thêm một scope function trong model, các bạn có thể xây dựng một hàm filter. Ví dụ như thế này:

 

public function index(Request $request)
{
    $user = User::filter($request);
    return $user->get();
}

 

Các bạn nhớ là không nên khai báo hàm filter nhiều lần. Do nó chỉ có chức năng trung chuyển và gọi các hàm filter ở mỗi model. Các bạn cần triển khai nó bằng trait để các model có thể dùng chung với nhau. Ví dụ bạn tạo 1 trait filterable như sau: 

 

<?php
namespace App\Traits;
use App\Filters\QueryFilter;
use Illuminate\Database\Eloquent\Builder;
trait Filterable
{
    public function scopeFilter(Builder $builder, QueryFilter $filters, array $filterFields = ['*'], array $orderFields = [])
    {
        return $filters->apply($builder, $filterFields, $orderFields);
    }
}

 

Ở mỗi model sẽ có 1 trait. Các bạn cũng nên tạo thêm 1 class base filter và đặt tên cho nó là Query Filter. 

 

<?php
namespace App\Filters;
use App\Helpers\Common\StringHelper;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Http\Request;
use Illuminate\Support\Str;
class QueryFilter
{
    /**
     * @var  Request
     */
    public $request;
    /**
     * @var  array
     */
    protected $filters;

    /**
     * @var  array
     */
    protected $search = [];
    /**
     * @var  $builder
     */
    protected $builder;
    /**
     * @var  string|null
     */
    protected $orderField = null;
    /**
     * @var  string
     */
    protected $orderType = 'desc';
    /**
     * @var  $filterable
     */
    protected $filterable;
    /**
     * QueryFilter constructor.
     * @param  Request $request
     */
    public function __construct(Request $request)
    {
        $this->request = $request;
        $this->filters = $this->request->all();
    }
    /**
     * @param  Builder $builder
     * @param  array $filterFields
     * @param  array $orderFields
     * @return  Builder
     */
    public function apply(Builder $builder, array $filterFields, array $orderFields = [])
    {
        $this->builder = $builder;
        $this->orderFields = $orderFields;
        foreach ($this->filters as $name => $value)
        {
            $method = 'filter' . Str::studly($name);

            if (is_null($value) || $value == '') {
                continue;
            }
            if (method_exists($this, $method)) {
                $this->{$method}($value);
                continue;
            }
            if (empty($this->filterable) || !is_array($this->filterable)) {
                continue;
            }
            if (in_array($name, $this->filterable)) {
                $this->builder->where($name, $value);
                continue;
            }
            if (key_exists($name, $this->filterable)) {
                $this->builder->where($this->filterable[$name], $value);
                continue;
            }
        }
        return $this->builder;
    }
}

 

Các bạn có thể tạo một class filter riêng từng mode user. Ví dụ như sau: 

 

<?php
namespace App\Filters;
class UserFilter extends QueryFilter
{
    protected $filterable = [
        'id',
        'author_id',
        'birth_day',
        'gender',
    ];
    public function filterName($name)
    {
        return $this->builder->where('name', 'like', '%' . $name . '%');
    }
}

 

Class User Filter được extends cùng với class Query Filter. Vì sao luồng xử lý từ lúc gọi function filter đến lúc mỗi class filter này sẽ được áp dụng với các model? Đầu tiên, ở model user, các bạn nên sử dụng trait filterable. 

 

Khi đó, model user sẽ gọi function scope filter ở trait. Điều đó có nghĩa là khi các bạn gọi đến User::filter() thì Laravel sẽ hiểu được các bạn đang cần gọi đến 1 local scope (tương tự như các bạn gọi từng scope). Mỗi lần gọi đến hàm filter và truyền vào 1 instance của class user filter. Ví dụ:

 

User::filter($userFilter)

 

Khi đó, hàm scope filter trong trait nên khai báo và sẽ được gọi đến function apply. Thuộc tính $filters đã được gán value và là 1 mảng của request mà người dùng gửi tới. Value đó là các giá trị người dùng đã gửi lên từ phía client để filter theo mong muốn của họ. Do đó, value có thể được gửi bằng form data hoặc json data. 

 

Ở class user filter, các bạn nên khai báo thêm 1 biến $filterable là một array – sẽ chứa các trường mà các bạn mong muốn theo điều kiện where('fieldName', value). Các bạn cũng nên khai báo function bắt đầu name là filter. Theo sau đó, name của field name camel case được gửi từ client. 

 

Ở mỗi vòng lặp, bạn sẽ xây query dựa vào value đã được gửi lên. Đồng thời, bạn dựa theo chức năng và các field name đã được khai báo trong biến thuộc tính filterable. Khi bạn gọi đến scope filter và truyền vào 1 instance của class user function 

 

Khi bạn gọi đến scope filter và truyền vào 1 instance của class user function này, thì bạn sẽ xây và quay lại ra câu query có instance là Builder. Sau đó, bạn chỉ cần gọi tới function get() ở cuối câu để lấy data như ý muốn cần filter. Ví dụ:

 

User::filter($userFilter)->get();

 

Bí kíp giúp hàm filter trông đẹp và gọn hơn

 

Để khiến cho hàm filter trông đẹp và gọn gàng hơn, các bạn loại trừ biến $request. Điều đó giúp cho nó không bị lệ thuộc và chỉ truyền vào mảng $param.

 

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

 

Các bạn có thể áp 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;
}

 

Ưu và nhược điểm khi query filter 

 

Vì sao cần query filter trong Laravel? Muốn biết, các bạn nên tìm hiểu ưu và nhược điểm của phương pháp này (trước khi áp dụng vào dự án thực tế).

 

Ưu điểm 

 

  • Dễ dàng sử dụng và tái sử dụng
  • Dễ triển khai thêm các filed mới
  • Có thể kết hợp dùng chung hàm filter với các hàm scope hoặc các hàm gốc của model theo syntax chaining method
  • Bản code gọn gàng và đẹp

 

Nhược điểm

 

  • Khó biết chính xác cái gì sẽ được filter
  • Phải kiểm tra kỹ đầu vào trước khi gọi hàm filter
  • Các bạn lập trình viên mới vào thì sẽ thấy khó tiếp cận

 

Vậy là các bạn đã hoàn tất triển khai một cơ chế filter đơn giản. Nếu muốn filter cho 1 model nào đó, thì các bạn chỉ cần dùng use Filterable. Khi gặp các field đơn giản, hãy khai báo trong biến $filterable. Ở các logic phức tạp hơn, các bạn tạo hàm filter{$field}() tương ứng và gọi hàm scope filter($param) là xong.

 

 

 

Hồ Hữu Hiền

Mình là developer nên đôi khi viết bài không hay lắm mong các bạn thông cảm. Nếu muốn biết thêm thông tin về mình thì vui lòng vào website này để biết. https://huuhienqt.dev/

Bình luận (0)