Macros, Mixins, and Custom Collection Methods in Laravel
#laravel #collections #macros #php

Macros, Mixins, and Custom Collection Methods in Laravel

3 min read Mohamed Said Mohamed Said

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 in register().
  • Use mixin classes to group related macros and keep providers lean.
  • $this inside a macro closure is bound to the instance — use it freely.
  • Add IDE stubs or @mixin annotations so static analysis tools stay happy.
  • Prefer macros for presentation and convenience helpers; keep domain logic in dedicated classes.

Found this useful?

Frequently Asked Questions

3 questions
Q01 Can a macro override an existing method on a Laravel class?
No. The `Macroable` trait only adds new methods. If a method already exists on the class, the macro is silently ignored. Use subclassing or decoration if you need to override existing behaviour.
Q02 How do I get IDE autocompletion for registered macros?
Add a `@mixin` PHPDoc annotation pointing to your mixin class, or use `barryvdh/laravel-ide-helper` which can generate `_ide_helper.php` stubs that include macro signatures for tools like PhpStorm.
Q03 Is there a performance cost to registering many macros?
Negligible. Macros are stored in a static array and resolved with a simple `isset` check. The cost is one closure allocation per macro at boot time, which is trivial compared to typical service container bindings.

Continue reading

More Articles

View all