skip to content

In Laravel, what is the difference between a Collection and a LazyCollection, and when would you reach for the lazy one?

level: juniorimportance: must knowfreq 42%

answer

  1. array in memory versus a source
  2. Illuminate\Support\LazyCollection
  3. nothing runs until enumeration
  4. collect()->lazy() and ->collect()
  5. no pop, shift or push on lazy

basics

~20 s

A Collection holds every item in a PHP array; a LazyCollection holds a source, usually a generator function, and pulls items one at a time only when enumerated. Use it when the data is too big for memory.

solid answer

~40 s

`Illuminate\Support\Collection` wraps an array that already sits in memory, and each method builds a new array. `Illuminate\Support\LazyCollection` wraps a *source* — typically a closure that `yield`s — and its methods such as `map()`, `filter()` and `take()` just stack new generator steps; nothing runs until something enumerates it (`each()`, `all()`, `count()`, a `foreach`). Each item then flows through the whole chain before the next is read, so memory stays roughly flat however long the source is. Both implement `Enumerable`, so most methods match, but in-place methods like `pop()`, `shift()` and `push()` are absent. Reach for lazy when reading huge files, streaming results or working with endless sequences; convert with `collect()->lazy()` and back with `->collect()`.

code

php · 16 lines
php
<?php

use Illuminate\Support\LazyCollection;

$lazy = LazyCollection::make(function () {
    foreach (range(1, 5) as $n) {
        echo "read {$n}\n";
        yield $n;
    }
});

$pipeline = $lazy->map(fn ($n) => $n * 10); // prints nothing yet

$pipeline->take(2)->all();
// read 1
// read 2

go deeper

for a junior

Recall that a LazyCollection pulls items one at a time from a generator and runs nothing until it is enumerated.

for a middle

Explain pipeline versus terminal methods, how collect()->lazy(), ->collect() and ->eager() convert, and why mutating methods are missing.

for a senior

Choose lazy only where memory is the constraint, and watch for steps that force full evaluation or rerun the source.

for a principal

Decide where streaming belongs in data-heavy features — imports, exports, reports — versus pushing the work into the database or a queue.

## Two classes, one contract Laravel ships two collection classes that implement the same **`Illuminate\Support\Enumerable`** contract: | | `Collection` | `LazyCollection` | |---|---|---| | What it stores | a PHP array of every item | a **source**: a generator function, an array or another lazy collection | | When methods run | immediately, producing a new array each time | when the result is **enumerated** | | Memory for N items | O(N) per intermediate step | roughly one item at a time for streaming steps | | In-place methods (`pop`, `shift`, `push`, `transform`) | available | **not available** | | Created by | `collect()`, `new Collection`, `Model::all()` | `LazyCollection::make()`, `collect()->lazy()`, query-side streaming methods | The Laravel docs say almost every `Collection` method is also available on `LazyCollection`, and explicitly warn that methods which mutate the collection, such as `shift`, `pop` and `prepend`, are not. ## How laziness works A `LazyCollection` is built from a **generator function** — a closure containing `yield`: ```php <?php use Illuminate\Support\LazyCollection; $numbers = LazyCollection::make(function () { for ($i = 1; $i <= 1_000_000; $i++) { yield $i; } }); $firstEvenSquares = $numbers ->filter(fn (int $n) => $n % 2 === 0) ->map(fn (int $n) => $n * $n) ->take(3) ->all(); // [4, 16, 36] with keys 1, 3, 5 ``` Nothing happens on the `filter`, `map` and `take` lines; each returns a new `LazyCollection` whose source is a generator wrapping the previous one. Only `all()` starts pulling. Item 1 is read, rejected by `filter`; item 2 passes, is squared, and is taken; and so on until `take(3)` stops asking. The loop never reaches item 7, let alone a million. ## Terminal versus pipeline methods 1. **Pipeline methods** return another lazy collection and do no work yet: `map`, `filter`, `reject`, `take`, `skip`, `chunk`, `values`, `tapEach`. 2. **Terminal methods** enumerate and return a value: `all`, `toArray`, `each`, `count`, `sum`, `reduce`, `first`, `last`. Some pipeline-looking methods have to see every item before yielding one (sorting is the classic case), which is where laziness quietly ends — that distinction is a topic of its own. ## Converting between the two - **`collect()->lazy()`** (the `lazy()` method on `Collection`) wraps the existing array in a `LazyCollection`. The original array is still in memory, but further filters and maps no longer allocate intermediate arrays — the docs use this for filtering a large in-memory collection cheaply. - **`->collect()`** on a lazy collection enumerates it into a normal `Collection`. - **`->eager()`** enumerates it into a `LazyCollection` backed by an array, so later enumerations do not rerun the source. - **`->all()`** returns a plain PHP array. ## When to choose which - **Collection:** the data is already in memory and modest in size, you need in-place methods, or you will iterate several times. - **LazyCollection:** the data comes from a stream — a multi-gigabyte file, an API cursor, a database stream — or is conceptually endless (`LazyCollection::times(INF)`), and you process it in one pass. A lazy collection is not automatically faster: generators add per-item overhead, and a small array is quicker to process eagerly. The win is **memory**, not speed. ## What stays the same Because both classes implement `Enumerable`, most code reads identically: - the same method names and callback signatures (`map(fn ($value, $key) => …)`); - the same higher-order messages, such as `$lazy->map->country`; - `toArray()` and `toJson()` work, enumerating the source to build the output; - both are `Macroable`, but each class has **its own** macro list, so a macro registered on `Collection` must be registered on `LazyCollection` too. Static constructors carry over as well: `LazyCollection::range(1, 1_000_000)` and `LazyCollection::times(INF)` build sources without allocating the numbers up front — the latter is an endless sequence that only a `take()`, `takeUntil()` or similar step makes finite. ## A common screen follow-up Interviewers often ask what `LazyCollection::make($generatorObject)` does. The constructor refuses a `Generator` object with an `InvalidArgumentException` ("Generators should not be passed directly to LazyCollection. Instead, pass a generator function."), because a generator object can be run only once while a generator *function* can be called again for every enumeration.

  • Why can't you call pop() or push() on a Laravel LazyCollection?
    Those methods change the stored items in place, and a lazy collection has no stored items to change — it holds a source that produces them on demand. The docs state that mutating methods such as `shift`, `pop` and `prepend` are not available; convert with `->collect()` first if you need them.
  • Does collect($rows)->lazy() reduce the memory used by $rows itself in Laravel?
    No. `lazy()` wraps the array that is already in memory. What it saves is the intermediate arrays: later `filter()` and `map()` calls stream over that array instead of building new copies at each step.

An eager Collection is a printed stack of every page on your desk; a LazyCollection is a printer queue that prints the next page only when you reach for it, so the desk never holds more than the page in your hand.

saying these in an interview costs you the question

  • Says a LazyCollection still ends up holding every item, just loaded in batches.
  • Believes calling map() on a LazyCollection runs the callback immediately.
  • Thinks LazyCollection is always faster than Collection.
  • Expects push() and pop() to work on a LazyCollection.
  • Claims collect()->lazy() frees the original array's memory.