Refactoring với Laravel Collection Macros: Hướng dẫn đầy đủ

· 5 min read

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:

  1. Laravel IDE Helper: Package phổ biến này thường sẽ nhận diện được nếu bạn regenerate docs.
  2. 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.

Bình luận