Laravel Collections Reference Guide

A reference guide to Laravel Collections: a fluent, chainable wrapper around arrays that every Eloquent query returns.

Creating a collection

Every Eloquent query that returns multiple rows (get(), a hasMany relation, etc.) already returns a Collection, not a plain array — so these methods are available immediately, without any extra step.

$collection = collect([1, 2, 3, 4, 5]);

$users = User::where('active', true)->get(); // also a Collection

Eloquent results actually come back as Illuminate\Database\Eloquent\Collection, a subclass that adds model-aware methods on top of everything below — $users->modelKeys(), $users->load('posts') to eager-load onto an already-fetched set, and $users->fresh() to re-pull every model from the database. A plain collect() array doesn't have these; every other method on this page works identically on both.

Transforming

Collection methods never mutate the original in place — each one returns a new Collection, leaving the source untouched. That's what makes chaining safe: you can build up a pipeline of filter → map → sortBy without worrying that an earlier step altered data a later step, or another part of the request, still relies on.

$names = $users->map(fn ($user) => $user->name);

$adults = $users->filter(fn ($user) => $user->age >= 18);

$emails = $users->pluck('email');
$emailsById = $users->pluck('email', 'id'); // ['1' => 'a@x.com', ...]

$byCountry = $users->groupBy('country');

// Re-key the collection itself by an attribute — one item per key, unlike groupBy
$usersById = $users->keyBy('id'); // [1 => User, 2 => User, ...]

$sorted = $users->sortBy('name');
$sorted = $users->sortByDesc('created_at');

$total = $users->sum('order_count');
$average = $users->avg('age');

// Flatten a nested map/filter into one pass, and collapse an array of arrays into one level
$tags = $posts->flatMap(fn ($post) => $post->tags)->unique();
$allTags = collect([[1, 2], [3, 4]])->collapse(); // [1, 2, 3, 4]

// Chain freely — every method above returns a new Collection
$topSpenderNames = $users
    ->filter(fn ($user) => $user->total_spent > 1000)
    ->sortByDesc('total_spent')
    ->pluck('name');

// Only apply a step conditionally, without breaking the chain
$results = $users
    ->when($request->filled('sort'), fn ($collection) => $collection->sortBy($request->input('sort')))
    ->when($onlyActive, fn ($collection) => $collection->where('active', true));

Reducing to a single value

$total = collect([10, 20, 30])->reduce(fn ($carry, $item) => $carry + $item, 0);

$hasAdmin = $users->contains(fn ($user) => $user->role === 'admin');
$firstAdmin = $users->first(fn ($user) => $user->role === 'admin');
$count = $users->count();

Splitting and combining

$chunks = $users->chunk(50); // for batching, e.g. sending emails

$unique = $users->unique('email');

$merged = $users->merge($moreUsers);

[$active, $inactive] = $users->partition(fn ($user) => $user->active);

Checking and inspecting

$users->isEmpty();
$users->isNotEmpty();
$users->contains('email', 'jane@example.com');
$users->toArray();
$users->toJson();

Debugging and side effects mid-chain

tap() lets you peek at (or act on) the collection at a point in the chain without interrupting it — the original collection continues to the next method regardless of what the closure returns. pipe() instead passes the collection through a closure and returns whatever that closure returns, useful for handing it off to another class.

$users
    ->filter(fn ($user) => $user->active)
    ->tap(fn ($collection) => Log::info('Active users: '.$collection->count()))
    ->sortBy('name');

// dd()/dump() work the same way — inspect the pipeline without breaking it
$users->filter(fn ($user) => $user->active)->dump()->sortBy('name');

Higher order messages

A shorthand for calling the same method or accessing the same property on every item, without writing a full closure.

$users->each->sendWelcomeEmail();
$total = $orders->sum->total;
$names = $users->map->name;

A real-world pipeline

Because every method returns a new collection, it's common to build up a small pipeline that reads almost like a sentence describing the business rule, rather than a loop full of if statements and an accumulator variable:

// Group this month's paid orders by customer, and list the top 5 spenders
$topCustomers = Order::where('status', 'paid')
    ->whereMonth('created_at', now()->month)
    ->get()
    ->groupBy('customer_id')
    ->map(fn ($orders) => [
        'customer' => $orders->first()->customer,
        'total' => $orders->sum('total'),
        'order_count' => $orders->count(),
    ])
    ->sortByDesc('total')
    ->take(5)
    ->values();

Written as a loop, this same logic needs a running array keyed by customer id, manual accumulation, an arsort(), and an array_slice() — all of it bookkeeping that has nothing to do with the actual rule being expressed. The collection version reads top to bottom as the rule itself.

Lazy collections

A regular Collection holds every item in memory at once, which is fine for a page of results but becomes a problem when processing something like a multi-million-row export or a large log file. LazyCollection implements the same fluent API but wraps a PHP generator, pulling and processing one item at a time instead of materialising the whole set up front.

use Illuminate\Support\LazyCollection;

// Reads and processes the file one line at a time, not all at once
LazyCollection::make(function () {
    $handle = fopen('access.log', 'r');
    while (($line = fgets($handle)) !== false) {
        yield $line;
    }
})
    ->filter(fn ($line) => str_contains($line, 'ERROR'))
    ->take(100)
    ->each(fn ($line) => echo $line);

// Eloquent's cursor() returns a LazyCollection, running one query
// and hydrating models one at a time instead of loading them all
foreach (User::cursor() as $user) {
    // memory stays flat regardless of table size
}

See the Eloquent guide for more on querying models, including cursor()'s counterpart, eager loading.

The trade-off is that a lazy collection can only be iterated efficiently once per pipeline and can't do things that require knowing the full set up front, like sortBy, without pulling everything into memory anyway. Reach for it specifically when the row count is large and the work is a straightforward per-item transformation or filter.