Kita sudah memakainya tanpa menyebut namanya: Builder::macro('whereLike', ...) di chapter Custom Eloquent Builders
menambah method baru ke class builder milik framework. Macro adalah kemampuan menambah method ke class yang bukan milik kita — Str, Collection, Response — tanpa subclass dan tanpa memotong berkas vendor. Framework tidak menyediakan Str::initials('Budi Santoso'), tidak menyediakan Collection::toUpper(), dan response()->errorJson(...) juga tidak ada di daftar method response
. Ketiga method itu akan kita buat dalam chapter ini, lalu menutupnya dengan diskusi jujur: kapan macro berhenti menjadi solusi yang tepat.
Cara Kerja Macroable #
Seluruh sulap macro berasal dari satu trait: Illuminate\Support\Traits\Macroable. Inti kerjanya hanya dua ArrayAccess dan dua magic method — method statis macro() menyimpan callable di property statis $macros, lalu __callStatic() dan __call() meneruskan pemanggilan method yang tidak dikenal ke callable tersebut:
<?php
// inti trait Macroable (dipangkas)
protected static $macros = [];
public static function macro($name, $macro)
{
static::$macros[$name] = $macro;
}
public static function __callStatic($method, $parameters)
{
if (! static::hasMacro($method)) {
throw new BadMethodCallException(
sprintf('Method %s::%s does not exist.', static::class, $method)
);
}
$macro = static::$macros[$method];
if ($macro instanceof Closure) {
$macro = $macro->bindTo(null, static::class);
}
return $macro(...$parameters);
}Ketika kita menulis Str::initials('Budi'), PHP tidak menemukan method initials di Str, lalu __callStatic() mengambil closure yang kita daftarkan dan menjalankannya. Trait juga menyediakan mixin() untuk mendaftarkan banyak method sekaligus dari satu class, hasMacro() untuk memeriksa registrasi, dan flushMacros() untuk membersihkan.
Class yang Bisa Di-macro #
Daftar class macroable bisa diaudit langsung dari kode vendor — pola use ...Macroable adalah penandanya:
rg -l "use Illuminate\\\\Support\\\\Traits\\\\Macroable;" vendor/laravel/framework/src/Illuminate/Hasilnya 79 class. Selain yang akan kita pakai (Str, Collection, ResponseFactory), beberapa yang paling sering di-macro di aplikasi nyata:
| Class | Kegunaan umum macro |
|---|---|
Illuminate\Support\Str dan Stringable | helper string domain (initials, slug kustom) |
Illuminate\Support\Collection dan LazyCollection | transformasi koleksi (toUpper, toCsv) |
Illuminate\Routing\ResponseFactory | bentuk response standar (errorJson) |
Illuminate\Database\Eloquent\Builder | helper query lintas model (whereLike) |
Illuminate\Http\Request | ekstraksi input khusus aplikasi |
Illuminate\Cache\Repository | operasi cache tambahan |
Tiga Macro dalam Praktik #
Ketiga macro didaftarkan di method boot() milik AppServiceProvider — provider selalu berjalan sebelum request atau command ditangani, sehingga registrasi dijamin selesai lebih dulu:
<?php
// app/Providers/AppServiceProvider.php — bagian boot()
public function boot(): void
{
Str::macro('initials', function (string $name): string {
return collect(explode(' ', $name))
->map(fn (string $word) => mb_strtoupper(mb_substr($word, 0, 1)))
->implode('');
});
Collection::macro('toUpper', function (): Collection {
/** @var Collection<int, string> $this */
return $this->map(fn (string $item) => mb_strtoupper($item));
});
Response::macro('errorJson', function (string $message, int $status = 422) {
return response()->json(['error' => $message], $status);
});
}Macro untuk class statis seperti Str dipanggil lewat __callStatic. Macro untuk instance seperti Collection dipanggil lewat __call, dan closure-nya otomatis ter-bindTo ke instance — itulah sebabnya $this di dalam toUpper merujuk koleksi yang sedang dipanggil. Hasil ketiganya:
<?php
echo Str::initials('Budi Santoso').PHP_EOL;
echo Str::initials('Siti Rahmawati Putri').PHP_EOL;
echo Str::hasMacro('initials') ? 'terdaftar' : 'tidak ada';
echo PHP_EOL;
echo json_encode(collect(['kopi susu', 'roti bakar'])->toUpper()).PHP_EOL;
$response = response()->errorJson('Data tidak ditemukan', 404);
echo 'status: '.$response->status().', isi: '.$response->getContent().PHP_EOL;Output yang Dihasilkan:
BS
SRP
terdaftar
["KOPI SUSU","ROTI BAKAR"]
status: 404, isi: {"error":"Data tidak ditemukan"}Macro errorJson langsung bisa dipakai di route karena response() mengembalikan ResponseFactory yang sama:
<?php
// routes/web.php — pemakaian macro errorJson
Route::get('/produk/{kode}', function (string $kode) {
if ($kode !== 'kopi-toraja') {
return response()->errorJson('Data tidak ditemukan', 404);
}
return response()->json(['nama' => 'Kopi Toraja 200g', 'harga_cents' => 1850]);
})->name('produk.show');Menguji Macro #
Macro terdaftar sekali per proses, sehingga satu feature test bisa menguji semuanya — string, koleksi, sampai route yang memakai errorJson:
Output yang Dihasilkan:
PASS Tests\Feature\MacrosTest
✓ str initials membuat inisial nama 0.08s
✓ collection to upper mengubah semua item 0.01s
✓ error json macro dari route 0.02s
✓ route dengan data valid tetap berjalan 0.01s
Tests: 4 passed (7 assertions)
Duration: 0.19sTest terakhir penting: memastikan macro tidak menabrak perilaku response yang sudah ada. Setiap macro baru wajib membawa test minimal dua jalur — jalur macro-nya sendiri dan jalur normal di sekitarnya.
Macro atau Ekstensi Biasa #
| Situasi | Pilihan yang lebih tepat |
|---|---|
Helper kecil, lintas banyak pemakaian (Str::initials) | Macro |
Method hanya relevan untuk satu model (PostBuilder::popular()) | Subclass atau custom builder |
| Method butuh dependency injection atau state | Service class biasa |
| Method perlu diuji dan didokumentasikan sebagai API publik paket | Class nyata, bukan macro |
Alasannya sederhana: method macro tidak terlihat di signature class. IDE tidak menawarkan Str::initials di autocomplete sampai kita memasang plugin ide-helper, dan rekan tim yang membaca Illuminate\Support\Str di vendor tidak akan menemukannya. Macro adalah API yang hidup di luar kode — kuat untuk helper kecil, mahal untuk logika bisnis.
Kita kini bisa melengkapi framework di titik mana pun: method string, transformasi koleksi, bentuk response, sampai query builder. Kapabilitas ekstensi ini menyiapkan kita pada bab-bab berikutnya yang menyusun pola lanjutan di atas fondasi yang sudah ada — mulai dari middleware yang lebih cermat sampai paket yang bisa dipublikasikan.