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

Extending Laravel Without Forking It

Laravel ships with a Macroable trait that lets you bolt new methods onto core classes at runtime. Used carelessly, this becomes a dumping ground in AppServiceProvider. Used well, it's a clean extension point that keeps your domain vocabulary inside the framework's fluent API.

This article covers three distinct patterns: standalone macros, mixin classes, and purpose-built Collection methods — with concrete examples and the tradeoffs of each.


Macros: One-Off Extensions

The Macroable trait is used by Collection, Builder, Request, Response, Str, Arr, and more. Registering a macro is straightforward:

use Illuminate\Support\Collection;

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

// Usage
$map = collect($rows)->toAssoc('id', 'name');

Inside the closure, $this is bound to the macro's host object, so you get full access to its public and protected API. PHPStan and IDE plugins won't know about it by default — address that with a @method annotation on a stub or a dedicated _ide_helper_macros.php file.


When you have five or more related macros, a mixin class keeps them cohesive and testable:

namespace App\Support\Mixins;

class CollectionMixin
{
    public function toAssoc(): \Closure
    {
        return function (string $keyField, string $valueField): array {
            return $this->mapWithKeys(
                fn ($item) => [$item[$keyField] => $item[$valueField]]
            )->all();
        };
    }

    public function groupByFirst(): \Closure
    {
        return function (string $key): self {
            return $this->groupBy(fn ($item) => $item[$key] ?? null);
        };
    }
}

Register the mixin once, typically in a focused service provider:

use Illuminate\Support\Collection;
use App\Support\Mixins\CollectionMixin;

Collection::mixin(new CollectionMixin());

mixin() reflects over every public method, calls it to retrieve the closure, and registers each as a macro. The result is identical to registering macros individually, but the code is organised and unit-testable in isolation.


Custom Collection Classes: When Macros Aren't Enough

For domain-specific pipelines, a typed custom collection beats macros every time. You get return-type safety, IDE completion, and no global side effects:

namespace App\Domain\Billing\Collections;

use Illuminate\Support\Collection;
use App\Domain\Billing\ValueObjects\Money;

/**
 * @extends Collection<int, \App\Domain\Billing\Models\Invoice>
 */
class InvoiceCollection extends Collection
{
    public function totalDue(): Money
    {
        return Money::ofMinorUnits(
            $this->sum(fn ($invoice) => $invoice->amount_due_cents)
        );
    }

    public function overdue(): static
    {
        return $this->filter(
            fn ($invoice) => $invoice->due_at->isPast() && ! $invoice->paid
        );
    }
}

Wire it to your Eloquent model so get() and all() return the typed collection automatically:

class Invoice extends Model
{
    public function newCollection(array $models = []): InvoiceCollection
    {
        return new InvoiceCollection($models);
    }
}

Now Invoice::where('user_id', $id)->get() returns an InvoiceCollection, and ->totalDue() is fully typed.


Organising Registration: Avoid the God Provider

Don't pile macros into AppServiceProvider. Instead:

  1. Create a MacroServiceProvider per domain or package.
  2. Register it in bootstrap/providers.php (Laravel 11+) or config/app.php.
  3. Keep each mixin class in app/Support/Mixins/ or inside the relevant domain folder.
// app/Providers/BillingMacroServiceProvider.php
public function boot(): void
{
    Collection::mixin(new CollectionMixin());
    Request::mixin(new RequestMixin());
}

This makes it trivial to extract a domain into a package later — the service provider and mixin classes move together.


Key Takeaways

  • Use standalone macros for one-off, cross-cutting helpers.
  • Use mixin classes when you have a cohesive group of related extensions; they're unit-testable and self-documenting.
  • Use custom Collection subclasses for domain-specific pipelines where return-type safety matters.
  • Register macros in dedicated service providers, not AppServiceProvider.
  • Generate IDE helper stubs (php artisan ide-helper:generate) or maintain @method annotations so static analysis tools stay accurate.

Found this useful?

Frequently Asked Questions

3 questions
Q01 Can I use macros on classes that don't use the Macroable trait?
No. Only classes that include the `Macroable` trait support `::macro()` and `::mixin()`. If you need to extend a non-macroable class, subclass it or use a decorator instead.
Q02 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-lived workers. Register them once in a service provider's `boot()` method and they remain available for the lifetime of the worker process.
Q03 When should I prefer a custom Collection subclass over a mixin?
Prefer a subclass when the methods are domain-specific, return typed values, or only make sense for a particular model. Mixins are better for generic, reusable helpers that apply across many collection types.

Continue reading

More Articles

View all