Featured image of post Laravel LazyCollection Loses Lazy Evaluation with Generator

Laravel LazyCollection Loses Lazy Evaluation with Generator

Passing a Generator directly to LazyCollection causes iterator_to_array to expand it all at once. Wrap it in a Closure to restore true lazy evaluation.

The Code I Thought Would Work

After reading the docs, I knew LazyCollection with Generator enables lazy loading. So I passed a generator directly in, expecting the loop to stop once it found $i > 5:

1
2
3
$this->assertEquals(6, LazyCollection::make($this->generator())->collapse()->first(function ($i) {
    return $i > 5;
}));

It didn’t. The loop ran through all 9 iterations before stopping.

Why

After digging into the LazyCollection source code, I figured it out. If the constructor receives something that isn’t a Closure, it falls through to getArrayableItems(). Since Generator implements Traversable, it gets fully expanded by iterator_to_array() all at once:

1
2
3
4
5
6
7
8
protected function getArrayableItems($items)
{
    // ...
    } elseif ($items instanceof Traversable) {
        return iterator_to_array($items);
    }
    // ...
}

The lazy loading effect is completely lost.

Eager vs Lazy Generator execution difference

The Correct Approach

Wrap it in a Closure so that LazyCollection receives “a function that produces a Generator” rather than an already-running Generator:

1
2
3
4
5
$this->assertEquals(6, LazyCollection::make(function () {
    return $this->generator();
})->collapse()->first(function ($i) {
    return $i > 5;
}));

This way only the first iteration runs before stopping – that’s true lazy loading.

The distinction is whether you pass the Generator itself or a Closure that produces one.