Laravel Macros, Mixins, and Custom Collection Methods That Actually Ship
#laravel #macros #collections #php

Laravel Macros, Mixins, and Custom Collection Methods That Actually Ship

3 min read Mohamed Said Mohamed Said

Why Macros Deserve More Respect

Most teams discover Macro when they want a one-liner on Str or Collection. They drop it in AppServiceProvider::boot, ship it, and move on. That works — until you run under Octane, add PHPStan, or onboard a developer who has no idea where ->toAssocBy() came from.

This article covers the three patterns that actually hold up in production: targeted macros, mixins, and typed Collection subclasses.


Pattern 1 — Targeted Macros in Focused Providers

Registering every macro in AppServiceProvider creates a god-provider. Instead, create a dedicated provider per domain.

// app/Providers/CollectionMacroServiceProvider.php
class CollectionMacroServiceProvider extends ServiceProvider
{
    public function boot(): void
    {
        Collection::macro('toAssocBy', function (string $key): Collection {
            /** @var Collection $this */
            return $this->keyBy($key);
        });

        Collection::macro('filterMap', function (Closure $callback): Collection {
            /** @var Collection $this */
            return $this->map($callback)->filter()->values();
        });
    }
}

Register it in bootstrap/providers.php (Laravel 11+) or config/app.php. The provider is small, testable, and easy to grep.

Octane Safety

Macros are stored in a static $macros array on the Macroable trait. Under Octane the worker boots once, so macros registered in boot() persist across requests — that is exactly what you want. The danger is re-registering inside a request lifecycle (e.g., inside a Livewire component). Keep macro registration in providers only.


Pattern 2 — Mixins for Cohesive Method Groups

A mixin is a plain class whose public methods become macros in bulk. Use it when you have five or more related methods.

// app/Support/CollectionDateMixin.php
/**
 * @mixin \Illuminate\Support\Collection
 */
class CollectionDateMixin
{
    public function betweenDates(): Closure
    {
        return function (string $field, Carbon $from, Carbon $to): Collection {
            /** @var Collection $this */
            return $this->filter(
                fn ($item) => data_get($item, $field) >= $from
                    && data_get($item, $field) <= $to
            )->values();
        };
    }

    public function latestBy(): Closure
    {
        return function (string $field): mixed {
            /** @var Collection $this */
            return $this->sortByDesc($field)->first();
        };
    }
}
// In your provider
Collection::mixin(new CollectionDateMixin());

The @mixin docblock is picked up by Laravel IDE Helper and PHPStan (with the larastan/larastan extension), giving you autocompletion and type inference without extra stubs.


Pattern 3 — Typed Collection Subclasses (The Underused One)

For domain-specific collections, a subclass beats a macro every time. You get real return types, no @var hacks, and Eloquent integrates natively.

// app/Domain/Billing/InvoiceCollection.php
/**
 * @extends Collection<int, Invoice>
 */
class InvoiceCollection extends Collection
{
    public function totalOwed(): Money
    {
        return $this->reduce(
            fn (Money $carry, Invoice $invoice) => $carry->add($invoice->amount),
            Money::of(0, 'GBP')
        );
    }

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

Tell Eloquent to use it:

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

Now Invoice::all() returns InvoiceCollection, and PHPStan knows it.


Testing Your Macros

// tests/Unit/CollectionMacroTest.php
it('filterMap removes null results', function () {
    $result = collect([1, 2, 3, 4])
        ->filterMap(fn ($n) => $n % 2 === 0 ? $n * 10 : null);

    expect($result->values()->all())->toBe([20, 40]);
});

Unit-test macros in isolation — no HTTP, no database. Fast feedback, easy CI.


Takeaways

  • Register macros in dedicated providers, never inside request-scoped code.
  • Use mixins when you have a cohesive group of five or more methods; the @mixin docblock unlocks IDE and static analysis support.
  • Prefer typed Collection subclasses for domain models — they give real generics, no magic strings, and Eloquent wires them up automatically.
  • Macros survive Octane restarts because they live in static state bootstrapped once per worker.
  • Always write a unit test per macro; they are trivial to test and painful to debug silently broken.

Found this useful?

Frequently Asked Questions

3 questions
Q01 Are Laravel macros safe to use under Octane?
Yes, as long as you register them in a service provider's boot() method. Macros are stored in static arrays that persist for the worker's lifetime, which is exactly what you want. Never register macros inside request-scoped code like controllers or Livewire components.
Q02 When should I use a mixin versus a typed Collection subclass?
Use a mixin when you want to add utility methods to the base Collection class for general use across the app. Use a typed subclass when the collection belongs to a specific domain model — you get real generic type hints, PHPStan support, and Eloquent integration via newCollection().
Q03 How do I get PHPStan to recognise custom macros?
Add the @mixin docblock to your mixin class and ensure larastan/larastan is installed. For standalone macros, you can write a PHPStan extension or use IDE Helper's generated _ide_helper.php, which larastan reads automatically.

Continue reading

More Articles

View all