Refactoring với Laravel Collection Macros: Hướng dẫn đầy đủ
Refactoring với Laravel Collection Macros
Laravel Collections được cho là tính năng tốt nhất của framework này. Chúng biến đổi việc xử lý mảng lộn xộn thành các chuỗi (chains) trôi chảy, dễ đọc. Nhưng khi ứng dụng của bạn phát triển, bạn thường thấy mình lặp đi lặp lại cùng một chuỗi map, filter, và reduce ở nhiều nơi.
Bạn có thể tạo một lớp Helper. Bạn có thể đưa nó vào một Service. Nhưng "cách Laravel" nhất là dạy cho class Collection những chiêu thức mới bằng cách sử dụng Macros.
Macro là gì?
Trait Macroable (được sử dụng bởi Collections, Request, Response, v.v.) cho phép bạn đăng ký các phương thức tùy chỉnh tại thời điểm chạy (runtime). Về cơ bản, bạn có thể "monkey patch" các class cốt lõi của framework một cách an toàn.
Bước 1: Dọn dẹp AppServiceProvider
Các hướng dẫn thường bảo bạn đặt macro trong AppServiceProvider::boot(). Trong dự án thực tế, việc này sẽ nhanh chóng trở nên lộn xộn.
Hãy tạo một provider riêng:
php artisan make:provider CollectionMacroServiceProvider
Đăng ký nó trong bootstrap/providers.php (hoặc config/app.php).
// app/Providers/CollectionMacroServiceProvider.php
use Illuminate\Support\Collection;
use Illuminate\Support\ServiceProvider;
class CollectionMacroServiceProvider extends ServiceProvider
{
public function boot(): void
{
// Định nghĩa macro tại đây
Collection::macro('toUpper', function () {
return $this->map(fn ($value) => strtoupper($value));
});
}
}
Ví dụ thực tế
1. Macro "Chuẩn hóa Key"
API thường trả về các key lộn xộn (First_Name, last-name). Hãy sửa chúng thành snake_case.
Collection::macro('standardizeKeys', function () {
return $this->mapWithKeys(function ($value, $key) {
return [Str::snake($key) => $value];
});
});
// Sử dụng
$data = collect(['First_Name' => 'John', 'user-ID' => 1]);
$data->standardizeKeys(); // ['first_name' => 'John', 'user_id' => 1]
2. Fallback "Nếu Rỗng"
Đôi khi bạn muốn trả về mặc định sai khi collection trống, nhưng muốn giữ nguyên chuỗi xử lý.
Collection::macro('ifEmpty', function (callable $callback) {
if ($this->isEmpty()) {
$callback($this);
}
return $this;
});
3. Logic phức tạp: Tích Descartes
Macro rất hoàn hảo cho các phép toán phức tạp mà bạn không muốn copy-paste.
Collection::macro('cartesianProduct', function ($collection) {
return $this->flatMap(function ($a) use ($collection) {
return $collection->map(function ($b) use ($a) {
return [$a, $b];
});
});
});
Tổ chức với Mixins
Nếu bạn có 20 macro, một file vẫn là quá lớn. Hãy dùng Mixins. Một Mixin là một class mà các method trả về Closures.
// app/Macros/CollectionMixins.php
class CollectionMixins
{
public function paginate()
{
return function ($perPage = 15, $page = null, $options = []) {
$page = $page ?: (Paginator::resolveCurrentPage() ?: 1);
return new LengthAwarePaginator(
$this->forPage($page, $perPage),
$this->count(),
$perPage,
$page,
$options
);
};
}
public function extractEmails()
{
return function () {
return $this->pluck('email')->filter()->unique();
};
}
}
Đăng ký nó gọn gàng hơn nhiều:
Collection::mixin(new \App\Macros\CollectionMixins());
Giải quyết vấn đề Autocomplete của IDE
Nhược điểm lớn nhất của macro là PHPStorm/VS Code không biết chúng tồn tại. Bạn có hai giải pháp:
- Laravel IDE Helper: Package phổ biến này thường sẽ nhận diện được nếu bạn regenerate docs.
- Manual PHPDoc: Tạo một file
_ide_macros.phpở thư mục gốc (không load bằng autoloader).
// _ide_macros.php
namespace Illuminate\Support {
/**
* @method self standardizeKeys()
* @method LengthAwarePaginator paginate(int $perPage = 15)
*/
class Collection {}
}
Unit Testing Macros
Macro là global state (trạng thái toàn cục), vì vậy test chúng đòi hỏi sự cẩn thận.
// tests/Unit/CollectionMacroTest.php
test('standardize keys macro works', function () {
$collection = collect(['My Key' => 1]);
expect($collection->standardizeKeys()->toArray())
->toBe(['my_key' => 1]);
});
Bởi vì Collection được load bởi quy trình khởi động của framework, macro của bạn sẽ có sẵn trong tất cả các bài test kế thừa TestCase.
Tóm tắt
- Đừng làm lộn xộn controller với logic lặp lại.
- Hãy chuyển logic sang Macros hoặc Mixins.
- Hãy tạo một ServiceProvider riêng.
- Hãy giúp IDE của bạn bằng PHPDocs.
Macros biến các ngôn ngữ miền cụ thể (domain language) của ứng dụng thành các công dân hạng nhất (first-class citizens) của framework.