Trong quá trình phát triển các dự án Laravel, chắc hẳn bạn đã không ít lần gặp phải tình trạng: Viết đi viết lại cùng một logic xử lý dữ liệu. Một trong những tình huống điển hình nhất là tính năng Tìm kiếm (Search). Khi hệ thống lớn dần, việc quản lý hàng chục Model với các câu lệnh truy vấn trùng lặp không chỉ làm file code dài ra mà còn tiềm ẩn những lỗi logic cực kỳ khó chịu. Hôm nay, chúng ta sẽ cùng tìm hiểu về Macroable — một tính năng mạnh mẽ giúp bạn “định nghĩa lại” cuộc chơi.
1. Vấn đề: Sự cồng kềnh của những câu Query thuần
Thông thường, khi cần tìm kiếm một từ khóa trên nhiều cột (ví dụ: name, email, address), chúng ta thường viết như sau:
$searchTerm = $request->input('q');
$users = User::where('name', 'LIKE', "%{$searchTerm}%")
->orWhere('email', 'LIKE', "%{$searchTerm}%")
->orWhere('address', 'LIKE', "%{$searchTerm}%")
->get();
Những rủi ro tiềm ẩn:
- Lỗi logic AND/OR: Nếu bạn thêm một điều kiện
where('status', 'active')vào sau cụm trên, SQL sẽ hiểu nhầm ý định của bạn. Thay vì tìm (A hoặc B hoặc C) VÀ phải Active, nó sẽ hiểu là (A) hoặc (B) hoặc (C và Active). Điều này dẫn đến kết quả tìm kiếm sai lệch hoàn toàn. - Vi phạm nguyên tắc DRY (Don’t Repeat Yourself): Nếu Model
Post,Product, hayOrdercũng cần tìm kiếm tương tự, bạn sẽ phải copy-paste đoạn code đó. Khi cần đổiLIKEthànhFull-text search, bạn phải đi sửa ở… 20 file khác nhau. - Khó đọc: Controller của bạn sẽ bị lấp đầy bởi những chi tiết triển khai (implementation details) thay vì tập trung vào luồng nghiệp vụ chính.
2. Giải pháp: Tại sao không dùng Scope mà lại dùng Macro?
Nhiều người sẽ nghĩ đến Local Scope. Tuy nhiên, Scope có một giới hạn về kiến trúc:
- Scope: Được khai báo bên trong Model. Nó mang tính chất nghiệp vụ. Ví dụ:
Order::isPaid()là logic chỉ dành cho Đơn hàng. - Macro: Được khai báo toàn cục (Global). Nó mang tính chất tiện ích. Một hàm như
whereLike()(tìm kiếm gần đúng) là nhu cầu chung của mọi bảng trong cơ sở dữ liệu.
Thay vì ép mọi Model phải kế thừa một Trait hoặc viết Scope riêng, chúng ta sử dụng Macro để “bơm” trực tiếp phương thức này vào Query Builder của Laravel.
3. Triển khai chi tiết Macro whereLike (Hỗ trợ cả Relationship)
Chúng ta sẽ tạo một MacroServiceProvider để quản lý. Macro này không chỉ giải quyết bài toán tìm kiếm cơ bản mà còn xử lý được cả các bảng liên quan (Relation) bằng dấu chấm (.).
namespace App\Providers;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Support\Arr;
use Illuminate\Support\ServiceProvider;
class MacroServiceProvider extends ServiceProvider
{
public function boot()
{
/**
* Macro tìm kiếm đa cột và đa bảng
* @param string|array $attributes Danh sách cột cần tìm (VD: 'name' hoặc ['name', 'category.title'])
* @param string $searchTerm Từ khóa tìm kiếm
*/
Builder::macro('whereLike', function ($attributes, string $searchTerm) {
// Bao bọc toàn bộ logic trong một closure where() để cô lập điều kiện OR
// Điều này tạo ra SQL dạng: WHERE ... AND (column1 LIKE %?% OR column2 LIKE %?%)
this->where(function (Builder $query) use ($attributes, $searchTerm) {
foreach (Arr::wrap($attributes) as $attribute) {
$query->orWhere(function (Builder $query) use ($attribute, $searchTerm) {
// Kiểm tra nếu attribute có chứa dấu chấm (Tìm kiếm trên quan hệ)
if (str_contains($attribute, '.')) {
[$relationName, $relationAttribute] = explode('.', $attribute);
// Sử dụng orWhereHas để truy vấn sang bảng liên quan
$query->orWhereHas($relationName, function (Builder $query) use ($relationAttribute, $searchTerm) {
$query->where($relationAttribute, 'LIKE', "%{$searchTerm}%");
});
} else {
// Tìm kiếm trên bảng hiện tại
$query->where($attribute, 'LIKE', "%{$searchTerm}%");
}
});
}
});
return $this; // Trả về Builder để tiếp tục chaining (ví dụ: ->orderBy())
});
}
}
4. Trải nghiệm thực tế sau khi áp dụng
Hãy xem cách code của bạn “lột xác” trong Controller. Giả sử bạn cần tìm kiếm bài viết theo tiêu đề, nội dung và cả tên tác giả (nằm ở bảng Users).
Cách cũ:
$posts = Post::where('title', 'like', "%$q%")
->orWhere('content', 'like', "%$q%")
->orWhereHas('author', function($query) use ($q) {
$query->where('name', 'like', "%$q%");
})
->where('is_published', true) // Dễ bị sai logic
->get();
Cách mới với Macro (Sạch hơn):
$posts = Post::whereLike(['title', 'content', 'author.name'], $q)
->where('is_published', true)
->latest()
->paginate(15);
Tại sao cách này tối ưu hơn?
- Tính đóng gói: Logic xử lý dấu chấm (relation) hay nhóm ngoặc đơn SQL đã được giấu kín bên trong Macro.
- Tính nhất quán: Mọi lập trình viên trong dự án đều dùng chung một cách gọi hàm, giúp việc bảo trì trở nên cực kỳ dễ dàng.
- Khả năng mở rộng: Nếu sau này bạn muốn đổi sang một bộ máy tìm kiếm khác (như Elasticsearch), bạn chỉ cần thay đổi logic tại một nơi duy nhất:
MacroServiceProvider.
5. Mở rộng cho Collection
Không chỉ Query Builder, Collection cũng cực kỳ mạnh mẽ khi kết hợp với Macro. Ví dụ, bạn có một mảng dữ liệu phức tạp và muốn trích xuất nhanh để làm dữ liệu cho dropdown:
use Illuminate\Support\Collection;
Collection::macro('toOptions', function ($label = 'name', $value = 'id') {
return $this->map(fn($item) => [
'label' => data_get($item, $label),
'value' => data_get($item, $value),
])->values();
});
// Sử dụng:
$userOptions = User::all()->toOptions('full_name', 'uuid');
Lời kết
Macroable là một “vũ khí bí mật” giúp bạn viết code Laravel một cách tinh tế hơn. Nó biến những thao tác lặp đi lặp lại thành những công cụ mạnh mẽ, giúp codebase của bạn luôn sạch sẽ và dễ hiểu.
Tuy nhiên, có một lưu ý nhỏ: Vì Macro là các phương thức được thêm vào khi chương trình đang chạy, IDE như VS Code hay PHPStorm sẽ không tự nhận diện được để gợi ý code. Bạn nên cài thêm package barryvdh/laravel-ide-helper để hỗ trợ việc viết code mượt mà hơn.
Bạn đã sẵn sàng để “Macro hóa” dự án của mình chưa? Hãy bắt đầu với những logic mà bạn thấy mình đang copy-paste nhiều nhất nhé!
Hy vọng bài viết này giúp ích cho lộ trình phát triển kỹ năng của bạn. Nếu có bất kỳ thắc mắc nào về cách triển khai nâng cao, đừng ngần ngại để lại bình luận!