Query filter trong laravel đơn giản như thế nào?

alt text

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 đề đó.

Local Scopes

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 FilterableUser

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 scopeName, scopeStatus... thành filterName, filterStatus... 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.

Giải pháp khác

Nếu bạn cảm thấy bài viết này khó tiếp thu quá thì mình có thể gợi ý một số package hỗ trợ filter:

Hoặc một số bài viết nâng cao về filter với tư tưởng khác

Nguồn: https://reishou.wordpress.com/2019/05/17/query-filter-trong-laravel-don-gian-nhu-the-nao/

Bình luận


White
{{ comment.user.name }}
Bỏ hay Hay
{{comment.like_count}}
Male avatar
{{ comment_error }}
Hủy
   

Hiển thị thử

Chỉnh sửa

White

Reishou

6 bài viết.
4 người follow
Kipalog
{{userFollowed ? 'Following' : 'Follow'}}
Cùng một tác giả
White
2 0
Tạo file trait App\Http\Response.php $value) { $thisdata$key] = $value; } } public function setHeaders(array $headers) { foreach ($headers...
Reishou viết 11 tháng trước
2 0
White
1 2
Cài đặt môi trường lập trình web trên macOS khá thú vị. Có rất nhiều cách, nhiều hướng, nhiều stack cài đặt để lựa chọn. Nếu bạn muốn nhanh gọn thì...
Reishou viết 3 tháng trước
1 2
White
1 0
Với phần 1 và phần 2 thì các bạn có thể cài đặt hầu hết các dự án php thông thường. Tuy nhiên với các dự án hơi đặc thù lại cần một số php extensio...
Reishou viết 3 tháng trước
1 0
Bài viết liên quan
White
13 3
Nhiều khi chúng ta cần một start một project nhỏ gọn, không cần phải quá cầu kỳ, nhưng lại quá quen thuộc với Eloquent của Laravel. Vậy làm sao để ...
Ôm Boom viết hơn 2 năm trước
13 3
{{like_count}}

kipalog

{{ comment_count }}

bình luận

{{liked ? "Đã kipalog" : "Kipalog"}}


White
{{userFollowed ? 'Following' : 'Follow'}}
6 bài viết.
4 người follow

 Đầu mục bài viết

Vẫn còn nữa! x

Kipalog vẫn còn rất nhiều bài viết hay và chủ đề thú vị chờ bạn khám phá!