Paulund
Subscribe

Custom Collections in Laravel

2 March 2026, 2 min read

From the archive. Versions and APIs may have moved on since I wrote this.

On this page
  1. Extending the Base Collection
  2. Returning Custom Collections from Eloquent
  3. A Practical Example
  4. When to Use Custom Collections
  5. When Not to Use Them
  6. Tips

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:

PHP
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:

PHP
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:

PHP
$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:

PHP
// ❌ 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:

PHP
// ✅ 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:
PHP
class InvoiceService
{
    public function getMonthlyInvoices(): InvoiceCollection
    {
        return Invoice::where('issued_at', '>=', now()->subMonth())->get();
    }
}

Next in Laravel

2 March 2026Eloquent Builder vs Scopes in Laravel

Tags

  • Laravel
  • Collections
  • Best practices