Why Extend Collections at All?
Laravel's Collection class covers the 90 % case, but every domain has its own vocabulary. Repeating ->filter(fn($u) => $u->isActive())->values() across ten service classes is a smell. Macros and mixins let you encode that vocabulary once and test it in isolation.
Macros: The Quick Win
Collection uses the Macroable trait, so you can attach a closure at boot time:
// app/Providers/CollectionServiceProvider.php
use Illuminate\Support\Collection;
use Illuminate\Support\ServiceProvider;
class CollectionServiceProvider extends ServiceProvider
{
public function boot(): void
{
Collection::macro('active', function (): Collection {
/** @var Collection $this */
return $this->filter(fn($item) => $item->is_active)->values();
});
Collection::macro('keyedById', function (): Collection {
return $this->keyBy('id');
});
}
}
Register the provider in bootstrap/providers.php (Laravel 11+) and you can write:
$users->active()->keyedById();
The closure's $this is the collection instance — no static tricks needed.
Mixins: Organising Many Macros
Once you have more than a handful of macros, a mixin class keeps things tidy. Each public method returns a Closure:
// app/Collections/UserCollectionMixin.php
class UserCollectionMixin
{
public function active(): Closure
{
return function (): Collection {
return $this->filter(fn($u) => $u->is_active)->values();
};
}
public function admins(): Closure
{
return function (): Collection {
return $this->filter(fn($u) => $u->role === 'admin')->values();
};
}
public function totalRevenue(): Closure
{
return function (): int|float {
return $this->sum('revenue_cents') / 100;
};
}
}
Register it with one line:
Collection::mixin(new UserCollectionMixin());
IDE support is the catch. Add a @mixin docblock or generate an IDE helper via barryvdh/laravel-ide-helper to keep autocomplete intact.
Typed Domain Collections
For stricter guarantees, extend Collection directly and override offsetSet:
// app/Collections/OrderCollection.php
use Illuminate\Support\Collection;
use App\Models\Order;
/**
* @extends Collection<int, Order>
*/
class OrderCollection extends Collection
{
public function pending(): static
{
return $this->filter(fn(Order $o) => $o->status->isPending())->values();
}
public function totalGross(): int
{
return $this->sum('gross_amount_cents');
}
}
Tell Eloquent to use it on the model:
class Order extends Model
{
public function newCollection(array $models = []): OrderCollection
{
return new OrderCollection($models);
}
}
Now Order::where(...)->get() returns an OrderCollection automatically — no casting required at the call site.
Higher-Order Proxies
Laravel ships higher-order proxies for a fixed set of methods (map, filter, each, etc.). You cannot add new proxy targets, but you can combine them with your macros cleanly:
$orders->pending()->each->markAsProcessing();
// equivalent to
$orders->pending()->each(fn(Order $o) => $o->markAsProcessing());
The proxy delegates the method call to every item in the collection — useful for side-effect pipelines.
Testing Your Extensions
Macros and typed collections are trivial to unit-test with Pest:
it('filters active users', function () {
$users = collect([
(object) ['is_active' => true],
(object) ['is_active' => false],
]);
expect($users->active())->toHaveCount(1);
});
it('returns an OrderCollection from eloquent', function () {
$orders = Order::factory(3)->create();
expect(Order::all())->toBeInstanceOf(OrderCollection::class);
});
Keep macro registration in a service provider so tests that boot the application pick it up automatically.
Key Takeaways
- Use macros for one-off, cross-domain helpers; use mixins to group related macros by domain.
- Use typed collection subclasses when you want static analysis, strict typing, and IDE autocomplete without extra packages.
- Register everything in a dedicated
CollectionServiceProvider— notAppServiceProvider— to keep boot logic focused. - Higher-order proxies work seamlessly alongside custom macros for expressive side-effect pipelines.
- Write a Pest unit test for every macro; they are pure functions and test in milliseconds.