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.phpso PHPStorm resolves the methods.
Mixins: Grouping Related Macros
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.
LazyCollectionhas its ownMacroabletrait — 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.