Custom Collections
Custom Collections
Laravel's Collection class is already powerful, but there are times when your domain logic benefits from its own dedicated collection. Custom collections let you attach business-specific behaviour to a group of related objects, keeping that logic out of controllers and services.
Extending the Base Collection
Create a class that extends Illuminate\Support\Collection and add methods that make sense for your domain:
namespace App\Collections;
use Illuminate\Support\Collection;
use App\Models\Order;
class OrderCollection extends Collection
{
public function totalRevenue(): float
{
return $this->sum('total');
}
public function completedOrders(): self
{
return new self($this->filter(fn(Order $order) => $order->status === 'completed'));
}
public function averageOrderValue(): float
{
return $this->isEmpty() ? 0.0 : $this->totalRevenue() / $this->count();
}
public function groupByCustomer(): array
{
return $this->groupBy('customer_id')->map(fn(Collection $orders) => new self($orders));
}
}
Returning Custom Collections from Eloquent
Tell your model to use your custom collection by overriding the newCollection method:
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
use App\Collections\OrderCollection;
class Order extends Model
{
public function newCollection(array $models = []): OrderCollection
{
return new OrderCollection($models);
}
}
Now every time you call Order::get() or access a hasMany relationship, you get an OrderCollection back:
$orders = Order::where('status', 'completed')->get();
// These methods are available directly on the result
$revenue = $orders->totalRevenue();
$average = $orders->averageOrderValue();
A Practical Example
Suppose you are building a reporting page that displays invoice statistics. Without a custom collection, the logic lives scattered across a controller or a service class:
// ❌ Without a custom collection — logic spread across the controller
$invoices = Invoice::where('issued_at', '>=', now()->subMonth())->get();
$total = $invoices->sum('amount');
$paid = $invoices->filter(fn($i) => $i->status === 'paid');
$overdue = $invoices->filter(fn($i) => $i->status === 'overdue');
With a custom collection, that same logic is encapsulated:
// ✅ With a custom collection — clean and reusable
$invoices = Invoice::where('issued_at', '>=', now()->subMonth())->get();
$total = $invoices->totalAmount();
$paid = $invoices->paid();
$overdue = $invoices->overdue();
When to Use Custom Collections
- When you find yourself writing the same filtering or aggregation logic in multiple places.
- When a group of models has business rules that apply to the collection as a whole, not just individual items.
- When you want to make your code more readable by giving common operations meaningful names.
When Not to Use Them
- For one-off operations. A single
->filter()call in a controller does not warrant a custom collection. - When the logic is already well-served by Eloquent scopes or query builders. Custom collections operate on already-fetched data; scopes operate at the database level.
Tips
- Return
new self(...)from your custom collection methods so that chaining continues to return your custom type. - Type-hint your collection in method signatures to get IDE autocompletion:
class InvoiceService
{
public function getMonthlyInvoices(): InvoiceCollection
{
return Invoice::where('issued_at', '>=', now()->subMonth())->get();
}
}