0

Macro — runtime extension of a class without modifying its source

Intermediate5 min read·eng-14-007
interviewlaravel-src

Concept

Macro — in Laravel, a runtime extension added to a class without modifying its source code. Macros let you add methods to core Laravel classes from your service providers.

What "macro" means: The word comes from macro-programming — a way to extend a system at a meta-level. In Laravel, it's the Macroable trait that powers this.

How it works: Classes that use the Macroable trait (e.g., Request, Response, Collection, Builder, Str) expose macro() and mixin() static methods. When you call SomeClass::macro('methodName', fn), Laravel stores the closure. When $instance->methodName() is called later, __call() intercepts it and invokes the stored closure with $this bound to the instance.

Where to register macros: In a service provider's boot() method. Always use boot(), not register()boot() runs after all providers are registered.

What classes are Macroable in Laravel:

  • Illuminate\Support\Collection
  • Illuminate\Http\Request
  • Illuminate\Http\Response
  • Illuminate\Database\Query\Builder
  • Illuminate\Database\Eloquent\Builder
  • Illuminate\Support\Str
  • Illuminate\Support\Arr
  • Illuminate\Routing\Router

mixin(): Registers all public methods of a mixin class as macros at once. Useful when you have many related macros to add.

Trade-offs:

  • Great for adding methods to framework classes in packages.
  • Not statically analyzed — IDEs and Psalm/PHPStan won't know about macro methods without stubs.
  • Use sparingly in application code — creating a custom class is often clearer.

Code Example

php
<?php
// In AppServiceProvider::boot() — register macros here

use Illuminate\Support\Collection;
use Illuminate\Http\Request;
use Illuminate\Support\Str;

// Collection macro — adds toAssoc() method
Collection::macro('toAssoc', function (string $key): Collection {
    return $this->keyBy($key); // $this = the Collection instance
});

$users = collect([
    ['id' => 1, 'name' => 'Alice'],
    ['id' => 2, 'name' => 'Bob'],
]);
$byId = $users->toAssoc('id'); // [1 => [...], 2 => [...]]

// Request macro — adds a helper for getting the API version
Request::macro('apiVersion', function (): int {
    $path = $this->path(); // $this = the Request instance
    if (preg_match('/^api\/v(\d+)/', $path, $m)) return (int) $m[1];
    return 1;
});

Route::get('/api/v2/users', function (Request $request) {
    $version = $request->apiVersion(); // 2 — from the URL path
});

// Str macro — adds a helper not in Laravel core
Str::macro('initials', function (string $name): string {
    return collect(explode(' ', $name))
        ->map(fn($part) => strtoupper($part[0]))
        ->implode('');
});
echo Str::initials('Alice Smith'); // 'AS'
echo Str::initials('John Paul Jones'); // 'JPJ'

// mixin() — register many macros from a class
class CollectionMixins
{
    public function second(): \Closure
    {
        return function () { return $this->skip(1)->first(); };
    }

    public function third(): \Closure
    {
        return function () { return $this->skip(2)->first(); };
    }

    public function sumBy(): \Closure
    {
        return function (string $key): int|float {
            return $this->sum($key);
        };
    }
}

Collection::mixin(new CollectionMixins());

collect([10, 20, 30])->second(); // 20
collect([10, 20, 30])->third();  // 30

// IDE helpers for macros — use a stub file or PHPDoc
/** @mixin \Illuminate\Support\Collection */
class CollectionIdeHelper
{
    public function toAssoc(string $key): \Illuminate\Support\Collection {}
}