Skip to content

Macros, Mixins, and Custom Collection Methods in Laravel

Learn how to extend Laravel's core classes with macros, mixins, and custom Collection methods — keeping your codebase expressive without forking the framework.

Published · Updated · 4 min read

Macros, Mixins, and Custom Collection Methods in Laravel

Extending Laravel Without Forking It

Laravel ships with a Macroable trait that lets you bolt new behaviour onto core classes at runtime. Used well, it keeps domain language inside your codebase rather than scattered across helper files. Used carelessly, it turns a clean project into a maze of invisible methods. This article covers the mechanics, the guardrails, and the patterns that actually hold up at scale.


How Macroable Works

Any class that uses Illuminate\Support\Traits\Macroable gains two static methods: macro() and mixin(). At call time, __call and __callStatic proxy to the registered closure.

use Illuminate\Support\Collection;

Collection::macro('toAssoc', function (string $key, string $value): Collection {
    /** @var Collection $this */
    return $this->mapWithKeys(fn ($item) => [$item[$key] => $item[$value]]);
});

$result = collect([
    ['code' => 'USD', 'label' => 'US Dollar'],
    ['code' => 'EUR', 'label' => 'Euro'],
])->toAssoc('code', 'label');
// ['USD' => 'US Dollar', 'EUR' => 'Euro']

Inside the closure, $this is bound to the current collection instance, so you have full access to $this->items and every existing method.


Registering Macros the Right Way

Never register macros in a controller or a model. Always use a service provider so the macro is available on every request before any consumer needs it.

// app/Providers/CollectionMacroServiceProvider.php

namespace App\Providers;

use Illuminate\Support\Collection;
use Illuminate\Support\ServiceProvider;

class CollectionMacroServiceProvider extends ServiceProvider
{
    public function boot(): void
    {
        Collection::macro('second', function (): mixed {
            return $this->skip(1)->first();
        });

        Collection::macro('mapToModel', function (string $class): Collection {
            return $this->map(fn ($data) => new $class($data));
        });
    }
}

Add it to bootstrap/providers.php (Laravel 11+) or config/app.php providers array.


When you have a family of related methods, a mixin class is cleaner than a long list of macro() calls. Each public method on the mixin class returns a closure that becomes a macro.

// app/Mixins/CollectionCurrencyMixin.php

namespace App\Mixins;

use Illuminate\Support\Collection;

class CollectionCurrencyMixin
{
    public function sumMoney(): \Closure
    {
        return function (string $field, int $scale = 2): string {
            /** @var Collection $this */
            $total = $this->sum(fn ($item) => (int) round($item[$field] * (10 ** $scale)));
            return number_format($total / (10 ** $scale), $scale);
        };
    }

    public function formatAmounts(): \Closure
    {
        return function (string $field, string $currency = 'USD'): Collection {
            /** @var Collection $this */
            return $this->map(function ($item) use ($field, $currency) {
                $item[$field . '_formatted'] = $currency . ' ' . number_format($item[$field], 2);
                return $item;
            });
        };
    }
}
// In your service provider boot()
Collection::mixin(new CollectionCurrencyMixin());

IDE Support: Don't Skip This

Macros are invisible to static analysis. Add a @mixin docblock or generate an _ide_helper_macros.php file with barryvdh/laravel-ide-helper. For Psalm/PHPStan, create a stub:

// stubs/CollectionMacros.php  (excluded from autoload)

/** @mixin \Illuminate\Support\Collection */
class CollectionMacroStub
{
    public function toAssoc(string $key, string $value): \Illuminate\Support\Collection {}
    public function second(): mixed {}
}

Point your phpstan.neon at the stubs directory. Now your CI pipeline catches misuse.


Beyond Collections: Other Macroable Classes

Request, Response, Builder (query builder), Router, Str, Arr, and Carbon (via CarbonMixin) all support macros. A practical example — adding a typed helper to the query builder:

use Illuminate\Database\Query\Builder;

Builder::macro('whereUuid', function (string $column, string $uuid): Builder {
    /** @var Builder $this */
    return $this->where($column, '=', $uuid);
});

// Usage
User::query()->whereUuid('id', $request->uuid)->firstOrFail();

When Not to Use Macros

  • Complex logic with dependencies — inject a service instead; closures can't receive constructor injection cleanly.
  • Methods that need to be overridden per model — use a custom base model or a trait.
  • Anything that should be tested in isolation — macros are global state; prefer explicit classes for domain-critical logic.

Takeaways

  • Register all macros in a dedicated service provider, never inline.
  • Use mixin classes to group related macros and keep providers readable.
  • Add IDE stubs or @mixin annotations — macros without static analysis support create invisible debt.
  • Macroable is available on Request, Builder, Str, Router, and more, not just Collection.
  • Macros are global state; reserve them for genuinely cross-cutting, stateless helpers.

Frequently asked questions

Can I use dependency injection inside a Collection macro closure?

Not directly via constructor injection. You can call `app(MyService::class)` inside the closure, but if the logic is complex enough to need a service, extract it into a dedicated class and call that from the macro instead.

Do macros survive between Octane requests?

Yes — macros are registered on the class itself (stored in a static array), so they persist across requests in long-running processes like Octane. Register them once in a service provider and they remain available for the lifetime of the worker.

What is the difference between a macro and a mixin in Laravel?

A macro registers a single named closure on a Macroable class. A mixin registers all public methods of a class as macros in one call, which is cleaner when you have a family of related helpers to add.

Mohamed Said

About the author

Senior Backend Engineer specializing in Laravel, scalable SaaS platforms, APIs, and cloud infrastructure. I build secure, high-performance web applications that help businesses grow.

About GitHub LinkedIn WhatsApp Email Address My CV

Have a technical challenge?

Tell me what you’re building. I reply within two working days.

Discuss your project