Lesson 97 of 100
Iteration15 min5 checksPHP 8.5

Yield Values with Generators

Learn how PHP generators produce values lazily with yield, how to consume them safely, and how to turn bounded generators into arrays.

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.

Example
<?php
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";
Output
[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.

Example
<?php
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";
}
Output
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.

Example
<?php
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";
Output
[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.

Example
<?php
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";
Output
[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 $max is 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 yield returns a Generator and runs lazily.
  • Local state persists between yields.
  • Generators are forward-only, so call the function again to start over.
  • Use break or 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.
Before you start

Functions · Arrays · Types

You’ll practice
  • Consume bounded generators into JSON-compatible arrays
  • Implement and test a deterministic PHP 8.4 function
Your task

Make evenSquares() pass all 5 checks. Run your code as often as you like, then check your answer.

exercise.phpSaved in this browserPHP 8.5
CONSOLE & CHECKS● Ready

Ready when you are

Your output will appear here.
Make it work. Then make it pass.

PHP loads on your first run. Each check starts fresh. Your code stays in this browser; clearing site data removes your progress. A check or the task looks wrong? Report a problem with this exercise.