0

Model collections — Eloquent Collection vs base Collection

Intermediate5 min read·lv-12-019
laravel-src

Concept

When Eloquent returns multiple models, it wraps them in an Illuminate\Database\Eloquent\Collection — not a plain PHP array and not the base Illuminate\Support\Collection. The Eloquent Collection extends the base Collection and adds model-specific methods: find($id), contains($key), diff(), except($keys), only($keys), modelKeys(), load() (lazy eager loading), loadMissing(), loadCount(), and toQuery() (converts the collection back to an Eloquent builder constrained to those IDs).

The base Illuminate\Support\Collection is a general-purpose iterable wrapper with over 100 higher-order methods — map, filter, reduce, groupBy, sortBy, pluck, flatMap, chunk, each, reject, first, last, unique, and many more. Both are lazy-friendly: map and filter return new collection instances rather than mutating, following an immutable pipeline style. LazyCollection (from cursor() and lazy()) extends this with actual lazy evaluation using PHP generators.

The key architectural decision is: when to filter/transform in PHP vs in SQL. Pulling 10,000 rows into an Eloquent Collection to then call ->filter(fn ($u) => $u->is_active) is wasteful; that filter belongs in the WHERE clause. But if you already have a collection in memory (e.g., from an eager-loaded relationship), using Collection methods avoids a second database round-trip. toQuery() is the bridge: $users->toQuery()->update(['notified' => true]) issues a bulk UPDATE ... WHERE id IN (...) for exactly the models in your collection.

pluck() exists on both: User::pluck('email', 'id') hits the database and returns a plain Collection of email strings keyed by id. $collection->pluck('email') does the same in PHP on an already-retrieved collection. The SQL version is faster for large sets; the PHP version is correct when the collection is already in memory.

MethodAvailable onDescription
find($id)Eloquent Collection onlyO(n) search by primary key
modelKeys()Eloquent Collection onlyReturns array of primary key values
load() / loadMissing()Eloquent Collection onlyLazy eager load relationships
toQuery()Eloquent Collection onlyBuilder WHERE id IN (...)
map, filter, reduceBothFunctional transformations
groupBy, sortBy, pluckBothReshaping operations

Code Example

php
<?php

declare(strict_types=1);

use App\Models\User;
use Illuminate\Support\Collection;

// --- Eloquent Collection returned by all/get/paginate ---
$users = User::with('posts')->get(); // Illuminate\Database\Eloquent\Collection

// model-specific: find by PK without a DB query
$alice = $users->find(3); // searches in-memory, no SQL

// model-specific: lazy-load a relationship on the collection
$users->load('roles');
// SQL: SELECT * FROM roles INNER JOIN role_user ON ...
//      WHERE role_user.user_id IN (1, 2, 3, ...)

// model-specific: convert back to a query builder
$admins = $users->where('is_admin', true); // Collection filter, no SQL
$admins->toQuery()->update(['last_admin_audit' => now()]);
// SQL: UPDATE users SET last_admin_audit = ? WHERE id IN (1, 5, 9)

// --- Base Collection operations available on both ---
$emails = $users->pluck('email'); // Collection<string>

$byRole = $users->groupBy(fn (User $u) => $u->roles->first()?->name ?? 'none');
// Returns Collection where keys are role names and values are Collections of users

$active = $users->filter(fn (User $u) => $u->is_active); // no SQL
$sorted = $users->sortByDesc('created_at');               // in-memory sort

// --- LazyCollection (memory-efficient for large datasets) ---
$lazy = User::cursor(); // Illuminate\Support\LazyCollection backed by a generator
// SQL: SELECT * FROM users  (executed now, rows streamed one at a time)

$lazy->each(function (User $user): void {
    // Only one model in memory at a time
    $user->sendMonthlyDigest();
});

// --- Collection pipelines ---
$result = User::all()
    ->groupBy('country')
    ->map(fn (Collection $group) => $group->count())
    ->sortDesc()
    ->take(5);
// Returns top 5 countries by user count, computed in PHP
// Use DB::table('users')->selectRaw('country, COUNT(*) as cnt')
//     ->groupBy('country')->orderByDesc('cnt')->limit(5)->get()
// for large tables — SQL aggregation is always faster

Interview Q&A

Q: What is the difference between Illuminate\Database\Eloquent\Collection and Illuminate\Support\Collection, and when does each appear?

Illuminate\Support\Collection is the base, general-purpose iterable wrapper used throughout Laravel — for array wrapping, helper methods, and non-Eloquent data. Illuminate\Database\Eloquent\Collection extends it and adds methods that only make sense for model lists: find(), modelKeys(), load(), loadMissing(), loadCount(), and toQuery(). You get an Eloquent Collection whenever Eloquent hydrates multiple models — get(), all(), paginate()->items(), a relationship accessor on a parent. You get a base Collection from collect(), DB::table()->get() (which returns a collection of stdClass objects), or any explicit collect() call.


Q: When should you filter a collection in PHP versus pushing the filter into the SQL WHERE clause?

Push filters into SQL when: the collection has not been retrieved yet, the result set is large (thousands of rows), or the filter maps cleanly to a database predicate. Do PHP-side filtering when: the collection is already in memory (from an eager-loaded relationship), the filter depends on decrypted/cast values not queryable in SQL, or the filter involves complex PHP logic. A classic mistake is calling User::all()->filter(...) on a table with 500k rows — you load all rows into memory first. The correct version adds a ->where() to the query builder before calling ->get().


Q: What does toQuery() on an Eloquent Collection do and what is its use case?

toQuery() returns an Illuminate\Database\Eloquent\Builder instance constrained to WHERE id IN (...) for all the primary keys in the collection. It is the bridge between an in-memory collection and a bulk database operation. The canonical use case is: you retrieve and filter a collection in PHP, then want to update or delete exactly those models in a single SQL statement rather than looping and calling save()/delete() per model. For example: User::all()->filter(...)->toQuery()->update(['status' => 'reviewed']) — one UPDATE ... WHERE id IN (1, 3, 7) instead of N individual updates.