Macros, Mixins, and Custom Collection Methods in Laravel
Laravel's Macroable trait is one of the most underused extension points in the framework. Rather than subclassing Collection, Request, or Response, you can bolt on behaviour at boot time — keeping the framework's internals intact while making your application code read like a DSL.
How Macroable Works
Any class using the Macroable trait gains two static methods: macro() and mixin(). When you call a macro, Laravel resolves it from a static array and invokes it bound to the current instance — so $this works exactly as you'd expect.
use Illuminate\Support\Collection;
Collection::macro('toIndexedMap', function (string $key): array {
return $this->keyBy($key)->map->toArray()->all();
});
// Usage
$users = collect([
['id' => 1, 'name' => 'Alice'],
['id' => 2, 'name' => 'Bob'],
]);
$map = $users->toIndexedMap('id');
// [1 => ['id' => 1, 'name' => 'Alice'], 2 => [...]]
Register macros inside a service provider's boot() method so they're available for the entire request lifecycle.
Grouping with Mixins
Once you have more than a handful of macros, a mixin class keeps things tidy. A mixin is a plain class whose public methods each return a Closure. Laravel iterates those methods and registers each one as a macro.
namespace App\Support\Mixins;
use Illuminate\Support\Collection;
class CollectionMixin
{
public function paginateArray(): \Closure
{
return function (int $perPage, int $page = 1): array {
return [
'data' => $this->forPage($page, $perPage)->values()->all(),
'total' => $this->count(),
'page' => $page,
'pages' => (int) ceil($this->count() / $perPage),
];
};
}
public function mapToValueObjects(): \Closure
{
return function (string $class): static {
return $this->map(fn ($item) => new $class($item));
};
}
}
// AppServiceProvider::boot()
Collection::mixin(new \App\Support\Mixins\CollectionMixin());
IDE support is the usual concern. Add a @mixin annotation in a stub or use a package like barryvdh/laravel-ide-helper to generate PHPDoc for macros.
Extending Request and Response
The same pattern applies beyond Collection. Extending Request is especially useful for multi-tenant apps where you repeatedly resolve the current tenant.
use Illuminate\Http\Request;
Request::macro('tenant', function (): ?\App\Models\Tenant {
return $this->attributes->get('tenant');
});
// In a middleware
$request->attributes->set('tenant', $resolvedTenant);
// In a controller
$tenant = $request->tenant();
No inheritance, no custom FormRequest base class — just a clean extension point registered once.
Testing Macros
Because macros are registered globally, test them in isolation by calling the macro directly in a unit test. Pest makes this concise:
use Illuminate\Support\Collection;
it('paginates an array correctly', function () {
$result = collect(range(1, 25))->paginateArray(perPage: 10, page: 2);
expect($result['data'])->toHaveCount(10)
->and($result['total'])->toBe(25)
->and($result['pages'])->toBe(3);
});
Register the mixin in a beforeEach or in a dedicated TestCase base if it isn't already booted by the application.
When Not to Use Macros
- Complex domain logic — a dedicated service or action class is more testable and discoverable.
- Overriding existing methods — macros cannot override methods already defined on the class; they only add new ones.
- Cross-cutting concerns with dependencies — if your macro needs injected services, resolve them from the container inside the closure rather than relying on constructor injection.
Takeaways
- Register macros in service provider
boot()methods, never inregister(). - Use mixin classes to group related macros and keep providers lean.
$thisinside a macro closure is bound to the instance — use it freely.- Add IDE stubs or
@mixinannotations so static analysis tools stay happy. - Prefer macros for presentation and convenience helpers; keep domain logic in dedicated classes.