Laravel Eloquent Builder Macro

LAMP Dec 30, 2020

Hãy tưởng tượng trong một dự án thực tế bạn cần cung cấp một chức năng tìm kiếm cho người dùng. Với Eloquent bạn có thể thực hiện dễ dàng như sau:

User::query()
   ->where('name', 'LIKE', "%{$searchTerm}%") 
   ->orWhere('email', 'LIKE', "%{$searchTerm}%") 
   ->get();

Thao tác này sẽ trả về tất cả các bản ghi có tên hoặc email chứa chuỗi trong $searchTerm. Nếu bạn đang sử dụng MySQL, câu truy vấn này cũng sẽ được thực hiện theo cách không phân biệt chữ hoa hay chữ thường.

Sử dụng macro

Bây giờ, nếu bạn muốn thêm một kiểu tìm kiếm, không chỉ cho User model mà cho mọi model, bạn có thể thêm macro vào Query builder của Eloquent.

Bây giờ chúng ta cùng xây dựng macro xem thế nào nhé. Đầu tiên bạn viết nó trong function boot ở  App\Providers\AppServiceProvider hoặc trong một service provider của riêng bạn.

use Illuminate\Database\Eloquent\Builder;

// ...

Builder::macro('whereLike', function(string $attribute, string $searchTerm) {
   return $this->orWhere($attribute, 'LIKE', "%{$searchTerm}%");
});

Nào! giờ chúng ta có thể search where like đơn giản như sau:

User::query()
   ->whereLike('name', $searchTerm)
   ->whereLike('email', $searchTerm)
   ->get();

Nâng cấp macro


Làm như trên thì cũng không có gì đặc biệt lắm. Mình không thích việc bây giờ cứ phải lặp lại lệnh gọi whereLike đó cho mọi attribute muốn search. Vì thế Mình sẽ cải tiến nó một chút.

Builder::macro('whereLike', function($attributes, $searchTerm) {
   foreach(array_wrap($attributes) as $attribute) {
      $this->orWhere($attribute, 'LIKE', "%{$searchTerm}%");
   }
   
   return $this;
});

array_wrap là một hàm helper của Laravel. Khi cho một mảng, nó chỉ trả về mảng đó. Khi đưa vào một chuỗi chẳng hạn, nó sẽ bao bọc chuỗi đó trong một mảng. Vì vậy, kết quả sẽ luôn trả về là một mảng.

Macro ở trên có thể được sử dụng như sau:

// Search 1 column duy nhất
User::whereLike('name', $searchTerm)->get();

// Search nhiều column cùng lúc
User::whereLike(['name', 'email'], $searchTerm)->get();

Fixing macro

Macro trông đã khá ổn, nhưng nó có một bug khá khó chịu.

Hãy xem xét truy vấn này:

User::query()
   ->where('role', 'admin')
   ->whereLike(['name', 'email'], 'john')
   ->get();

Nếu bạn nghĩ rằng query sẽ chỉ trả về những user có role  là admin thì bạn đã nhầm. Bởi vì macro whereLike của mình chứa orWhere, dẫn đến việc câu truy vấn trên sẽ trả về mỗi user có role admin và tất cả user có tên hoặc email là john.

Chúng ta sẽ khắc phục điều đó bằng cách đặt orWhere trong một function. Điều này tương đương với việc đặt dấu ngoặc trong search query.

Builder::macro('whereLike', function ($attributes, string $searchTerm) {
    $this->where(function (Builder $query) use ($attributes, $searchTerm) {
        foreach (array_wrap($attributes) as $attribute) {
            $query->orWhere($attribute, 'LIKE', "%{$searchTerm}%");
        }
    });

    return $this;
});

Bây giờ truy vấn ở trên sẽ trả về tất cả các user có role admin và có tên hoặc email chứa john.

Thêm relationship cho macro


Vì hiện tại, chúng ta chỉ có thể search các attribute của model khi sử dụng trong phạm vi model đó. Và giờ ta sẽ thêm search attribute cả trong các relationship.

Builder::macro('whereLike', function ($attributes, string $searchTerm) {
    $this->where(function (Builder $query) use ($attributes, $searchTerm) {
        foreach (array_wrap($attributes) as $attribute) {
            $query->when(
                str_contains($attribute, '.'),
                function (Builder $query) use ($attribute, $searchTerm) {
                    [$relationName, $relationAttribute] = explode('.', $attribute);

                    $query->orWhereHas($relationName, function (Builder $query) use ($relationAttribute, $searchTerm) {
                        $query->where($relationAttribute, 'LIKE', "%{$searchTerm}%");
                    });
                },
                function (Builder $query) use ($attribute, $searchTerm) {
                    $query->orWhere($attribute, 'LIKE', "%{$searchTerm}%");
                }
            );
        }
    });

    return $this;
});

Với macro ở trên có thể sử dụng như sau:

Post::whereLike(['name', 'text', 'author.name', 'tags.name'], $searchTerm)->get();

Một ví dụ khác

Twitter: Sergio Bruder@sdbruder

Kết

Like và chia sẻ nếu thấy hay.

Tags

Great! You've successfully subscribed.
Great! Next, complete checkout for full access.
Welcome back! You've successfully signed in.
Success! Your account is fully activated, you now have access to all content.