Extending Laravel Collections: Macros, Mixins, and Higher-Order Proxies
#laravel #collections #macros #php

Extending Laravel Collections: Macros, Mixins, and Higher-Order Proxies

4 min read Mohamed Said Mohamed Said

Why Extend Collections at All?

Laravel's Collection class covers the common 80%, but real domain code always needs more. You could reach for a static helper, a standalone function, or a one-off map() chain — but each of those scatters intent across the codebase. Macros and mixins let you encode domain vocabulary directly on the collection, so call sites read like prose.


Macros: One-Off Domain Methods

A macro is the simplest extension point. Register it in a service provider and it becomes available on every Collection instance.

// app/Providers/CollectionServiceProvider.php
use Illuminate\Support\Collection;
use Illuminate\Support\ServiceProvider;

class CollectionServiceProvider extends ServiceProvider
{
    public function boot(): void
    {
        Collection::macro('toIndexedById', function (string $key = 'id'): Collection {
            /** @var Collection $this */
            return $this->keyBy($key);
        });

        Collection::macro('sumMoney', function (string $attribute): int {
            /** @var Collection $this */
            return $this->sum(fn ($item) => (int) data_get($item, $attribute));
        });
    }
}

Call sites are now expressive:

$invoices->toIndexedById();          // keyed by 'id'
$lineItems->sumMoney('amount_cents'); // domain-aware sum

IDE support tip: Add a /** @mixin \Illuminate\Support\Collection */ docblock to a stub class and reference it in your _ide_helper_macros.php so PHPStorm resolves the methods.


When you have a cluster of related methods, a mixin class keeps the service provider lean. Collection::mixin() reflects over every public method and registers each one as a macro.

// app/Collections/MoneyCollectionMixin.php
class MoneyCollectionMixin
{
    public function sumMoney(): Closure
    {
        return function (string $attribute): int {
            return $this->sum(fn ($item) => (int) data_get($item, $attribute));
        };
    }

    public function averageMoney(): Closure
    {
        return function (string $attribute): float {
            return $this->avg(fn ($item) => (int) data_get($item, $attribute)) ?? 0.0;
        };
    }

    public function formatAsCurrency(): Closure
    {
        return function (string $attribute, string $currency = 'USD'): Collection {
            return $this->map(function ($item) use ($attribute, $currency) {
                data_set($item, $attribute, number_format(
                    data_get($item, $attribute) / 100, 2
                ) . ' ' . $currency);
                return $item;
            });
        };
    }
}
// In the service provider:
Collection::mixin(new MoneyCollectionMixin());

Each public method returns a Closure; inside that closure $this is the Collection instance. This pattern scales cleanly — add a new mixin per bounded context.


Higher-Order Proxies: Chainable Magic

Laravel ships with higher-order proxies for methods like each, map, filter, reject, every, first, flatMap, groupBy, keyBy, max, min, partition, reject, skipUntil, skipWhile, sortBy, sortByDesc, sum, takeUntil, takeWhile, and unique. They let you call a property on the collection and chain a method name directly:

$orders->each->recalculateTotals();
$users->filter->isActive()->values();
$invoices->sortByDesc->createdAt();

This works because Collection::$proxies is a public static array. You can register your own model methods into it:

// In a service provider boot()
Collection::$proxies[] = 'approve';
Collection::$proxies[] = 'archive';

Now $invoices->each->approve() dispatches approve() on every item — no explicit closure needed.


Lazy Collections and Macros

LazyCollection is a separate class but also uses Macroable. Register macros on it independently if you need them on cursor-based result sets:

use Illuminate\Support\LazyCollection;

LazyCollection::macro('filterActive', function (): LazyCollection {
    return $this->filter(fn ($item) => $item->is_active);
});

Testing Your Macros

// tests/Unit/Collections/MoneyCollectionMixinTest.php
use Illuminate\Support\Collection;

it('sums money attributes in cents', function () {
    $items = Collection::make([
        ['amount_cents' => 1000],
        ['amount_cents' => 2500],
    ]);

    expect($items->sumMoney('amount_cents'))->toBe(3500);
});

Register the mixin inside a beforeEach or rely on the full application bootstrap — either works with Pest.


Key Takeaways

  • Macros are ideal for one-off domain methods; register them in a dedicated service provider.
  • Mixins group related macros into a class, keeping the provider clean and the logic testable.
  • Higher-order proxies eliminate boilerplate closures for single-method dispatches on collection items.
  • LazyCollection has its own Macroable trait — register macros on it separately when working with large datasets.
  • Always write unit tests for macros; they are pure functions and trivially testable with Pest.

Found this useful?

Frequently Asked Questions

3 questions
Q01 Can I use type hints and return types inside a Collection macro closure?
Yes, but because the closure is bound to the Collection instance at runtime, you cannot use `self` or `static` as return types. Use `Collection` explicitly, or omit the return type and rely on docblocks for IDE support.
Q02 Do macros registered on Collection also apply to Eloquent's Collection subclass?
No. `Illuminate\Database\Eloquent\Collection` extends the base Collection but has its own class. Register the macro on both classes, or register it only on the base class and call `parent::` methods — Eloquent Collection will inherit macros registered on the base via PHP's method resolution if the macro is not overridden.
Q03 Is there a performance cost to using macros over native Collection methods?
The overhead is a single `__call` dispatch and a closure invocation per macro call — negligible in practice. The real cost is always the underlying iteration, not the dispatch mechanism.

Continue reading

More Articles

View all