$ lexprog.com

// notes from an old coder -- php, databases, and the occasional rant

[March 14, 2026] Eloquent ORM

Eloquent Custom Collections

Eloquent Custom Collections: Tips & Tricks

────────────────────────────────────────────────────────

Eloquent Custom Collections: Tips & Tricks

Tip: Override newCollection()

class Post extends Model
{
    public function newCollection(array $models = []): PostCollection
    {
        return new PostCollection($models);
    }
}

Now Post::all() returns a PostCollection instead of a standard Collection.

Gotcha: Custom Collections Must Extend the Right Class

use Illuminate\Database\Eloquent\Collection;

class PostCollection extends Collection
{
    // Not the base Collection class
}

Tip: Add Domain-Specific Methods

class PostCollection extends Collection
{
    public function published(): static
    {
        return $this->filter(fn($p) => $p->published);
    }

    public function totalViews(): int
    {
        return $this->sum('views');
    }
}

Gotcha: Pagination Returns Standard Collections

Paginated results use LengthAwarePaginator, not your custom collection.

Tip: Collection Macros for Global Methods

Collection::macro('toUpper', function () {
    return $this->map(fn($v) => strtoupper($v));
});

Available on ALL collections, not just Eloquent.

Gotcha: map() Returns Base Collection

Even with a custom collection, map() returns a standard Collection. Override map() if needed.

Tip: High-Order Messages

$posts->each->publish();    // Calls publish() on each
$posts->pluck->title;       // Gets title from each
$users->sum->age;           // Sums age from each

Tip: Pipeline Pattern with Collections

$result = collect($data)
    ->filter(fn($i) => $i > 0)
    ->map(fn($i) => $i * 2)
    ->reduce(fn($carry, $i) => $carry + $i, 0);

Tip: Use cursor() for Memory-Neutral Iteration

When exporting 100K rows, get() loads everything into memory. cursor() uses yield and keeps memory flat regardless of row count. Perfect for artisan commands.

Tip: whereHas() vs load() — Two Different Things

whereHas() filters the parent query by relationship existence. load() eager-loads relationships AFTER the query. Mixing them up is a common source of logic bugs.

Gotcha: withCount() Adds a Subquery

withCount('comments') runs a correlated subquery on every row. On large tables, this can be slower than a separate query. Profile before relying on it.

Senior Insight

Custom collection methods via Macroable are a great way to add domain-specific logic to collections. I've built collections with ->toCsv(), ->toTree(), and ->partitionByStatus() methods that eliminated hundreds of lines of procedural code. The key: keep custom methods stateless and predictable. A collection method should return a new collection, not modify internal state. This preserves the chainability that makes collections powerful in the first place.

Source: Laravel Docs (https://laravel.com/docs/eloquent), Laravel News (https://laravel-news.com/), Freek.dev (https://freek.dev/tags/eloquent)

────────────────────────────────────────────────────────
<-- back to posts