01 · THE IDEAREAD → TRY → LEARN
Why generators exist
A normal function runs to a return and hands back one result. If you want a sequence, you usually build an array first and return all of it. That is fine for small lists, but it wastes memory and time when the sequence is large, or when the caller may only need the first few items.
A generator function looks like an ordinary function, but its body contains the yield keyword. Calling it does not run the body. It returns a Generator object, which is a forward-only iterator. Each time foreach asks for the next value, PHP resumes the body until the next yield, hands that value over, and pauses again with all local variables intact.
A first generator
The function below yields a countdown. The foreach loop collects the values into an array so we can print them as JSON.
function countdown(int $from): Generator {
for ($i = $from; $i >= 1; $i--) {
yield $i;
}
}
$values = [];
foreach (countdown(3) as $value) {
$values[] = $value;
}
echo json_encode($values), "\n";[3,2,1]
The loop variable $i keeps its value between yields. You never write the bookkeeping that a hand-made iterator class would need.
Execution is lazy
Nothing inside a generator body runs until the first value is requested. The next example prints messages so you can see the interleaving between the generator and its consumer.
function noisy(): Generator {
echo "start\n";
yield 'a';
echo "middle\n";
yield 'b';
echo "end\n";
}
$g = noisy();
echo "created\n";
foreach ($g as $v) {
echo "got $v\n";
}created start got a middle got b end
Notice that created appears before start. Creating the generator did no work at all.
Unbounded sequences need a stopper
Because values are produced on demand, a generator can describe a sequence that never ends. The consumer is then responsible for stopping, for example with break.
function naturals(): Generator {
$n = 1;
while (true) {
yield $n++;
}
}
$out = [];
foreach (naturals() as $v) {
if ($v > 5) {
break;
}
$out[] = $v;
}
echo json_encode($out), "\n";[1,2,3,4,5]
Never pass an unbounded generator to iterator_to_array(). It would try to collect values forever. Only convert generators that you know are finite, such as ones controlled by a $max argument.
Converting to an array and the key trap
A generator yields keys as well as values. If you write plain yield $value, PHP assigns automatic integer keys starting at 0. iterator_to_array($generator) keeps those keys by default, which causes surprises when yield from delegates to another generator whose keys overlap with the outer ones.
function inner(): Generator {
yield 1;
yield 2;
}
function outer(): Generator {
yield 0;
yield from inner();
yield 3;
}
echo json_encode(iterator_to_array(outer(), true)), "\n";
echo json_encode(iterator_to_array(outer(), false)), "\n";[1,3] [0,1,2,3]
With preserved keys, later values overwrite earlier ones that share a key, so values go missing. Passing false as the second argument discards the keys and gives you a clean list. For a plain sequence, that is almost always what you want.
Generators are single-pass
A generator cannot be rewound once it has moved past its first yield, and random access such as $gen[2] is not possible. If you need to loop twice, call the generator function again to get a fresh object, or store the results in an array.
Exercise contract
Write evenSquares(int $max): array. It must use a generator internally and return a list of squares of the positive even numbers from 2 up to and including $max, in ascending order.
evenSquares(10)returns[4, 16, 36, 64, 100].evenSquares(7)returns[4, 16, 36], because 7 is odd and the last even number is 6.- When
$maxis below 2, including negative values, return an empty array. - The returned array must be a list with keys 0, 1, 2 and so on.
Split the work in two: a generator function that yields each square, and evenSquares() that consumes it with iterator_to_array($gen, false). Check the boundary at $max carefully, since the final value must be included.
Recap
- A function containing
yieldreturns aGeneratorand runs lazily. - Local state persists between yields.
- Generators are forward-only, so call the function again to start over.
- Use
breakor a bound to stop sequences that could be infinite. - Use
iterator_to_array($gen, false)to collect a finite generator into a list, avoiding key collisions.
Functions · Arrays · Types
- Consume bounded generators into JSON-compatible arrays
- Implement and test a deterministic PHP 8.4 function