skip to content

In PHP, what does calling a function that contains yield return, and when does the code in its body actually run?

level: juniorimportance: must knowfreq 55%

answer

  1. the call itself runs no body code
  2. final internal class Generator
  3. each pull runs to the next yield
  4. automatic integer keys or yield $k => $v
  5. argument types still checked at the call

basics

~20 s

A function containing yield returns a Generator object when called, without running its body. Each foreach pull runs the body to the next yield, which hands out a value and pauses with the locals intact.

solid answer

~40 s

Any function whose body contains `yield` is a **generator function**. Calling it runs none of the body: PHP binds and type-checks the arguments, then returns an object of the final internal class `Generator`, which implements `Iterator`. When `foreach`, `current()`, `next()` or `send()` asks for a value, the body runs until the next `yield`, which hands out a value and freezes the function's locals and position. A bare `yield $v` gets an automatic integer key; `yield $key => $value` supplies its own. When the body reaches its end or a `return`, the generator is finished and the loop stops. Because code runs only on demand, validation written at the top of the body fails at the first iteration, not at the call.

code

php · 19 lines
php
<?php
declare(strict_types=1);

function countdown(int $from): Generator
{
    echo "start\n";
    for ($i = $from; $i > 0; $i--) {
        yield $i;
    }
    echo "done\n";
}

$gen = countdown(3);        // prints nothing
echo get_class($gen), "\n"; // Generator

foreach ($gen as $key => $value) {
    echo "$key => $value\n";
}
// start, 0 => 3, 1 => 2, 2 => 1, done

go deeper

for a junior

Recall that the call returns a Generator object and that the body runs only as foreach pulls values, pausing at each yield.

for a middle

Explain the run-to-next-yield cycle, how automatic and explicit keys are assigned, and why argument types are checked at the call while body validation is deferred.

for a senior

Show where the deferred start bites in real code, such as validation that fires in another layer or side-effect generators nobody iterates, and how a validating wrapper fixes it.

for a principal

Weigh when a lazy producer is worth its single-pass, no-count constraints for an API's callers versus returning a materialised array that is simpler to consume.

## A function that returns a paused function In PHP, the presence of the keyword `yield` anywhere in a function body changes what calling that function means. The function becomes a **generator function**, and calling it does not execute its statements. Instead, PHP returns an instance of `Generator`, a `final` class built into the engine that implements the `Iterator` interface. That object holds a suspended copy of the function: its parameters, its local variables and a position that starts before the first statement. The same applies to methods and closures: a method or an anonymous function containing `yield` returns a `Generator` when called. ## What happens at the call, and what happens later The sequence is easiest to see as numbered steps: 1. **The call.** PHP binds the arguments and checks them against the parameter types. A wrong type throws `TypeError` right here, at the call site. Then the engine creates the `Generator` and returns it. No line of the body has run. 2. **The first pull.** When a consumer asks for a value (a `foreach` loop starting, or a call to `current()`, `key()`, `valid()`, `rewind()` or `send()`), the body starts executing and runs until it reaches the first `yield`. 3. **Yield.** The expression after `yield` becomes the current value, a key is attached, and the function pauses. Its locals keep their values. 4. **Later pulls.** Each `next()` (which `foreach` calls between iterations) resumes the body right after the `yield` and runs to the next one. 5. **The end.** When the body falls off its last line or executes `return`, the generator is **finished**: `valid()` returns false and the `foreach` loop ends. ## Keys: automatic and explicit Every yielded value travels with a key, exactly as array elements do. | Form | Key the consumer sees | |---|---| | `yield $value;` | the next automatic integer, starting at 0 | | `yield $key => $value;` | `$key`, which may be any type, not only int or string | | `yield;` | the next automatic integer, with value `null` | The automatic counter continues from the largest integer key used so far, so `yield 'a'; yield 5 => 'b'; yield 'c';` produces keys 0, 5 and 6. A string key does not move the counter. ## Generator versus returning an array | Aspect | Function returning an array | Generator function | |---|---|---| | When the body runs | fully, during the call | piece by piece, as values are pulled | | Memory | the whole array exists at once | only what the body keeps in its locals | | Passes over the data | as many as you like | one forward pass per call | | Random access, `count()` | yes | no | | First result available | after all work is done | as soon as the first `yield` runs | A generator is the right tool when the sequence is large, expensive, or unbounded, and the consumer reads it once from start to end. ## Consequences at the call site The deferred start produces a few behaviours that surprise people new to generators: - **Validation fires late.** An `if (...) { throw ...; }` at the top of the body runs only when the first value is pulled, which may be in a different function, a different layer, or never. - **Forgetting to iterate means nothing happens.** A generator whose only job is side effects (writing rows, sending mail) does nothing if the caller discards the returned object. - **Argument type errors are not deferred.** Parameter checks happen at the call, because the engine creates the `Generator` only after the arguments are received. - **The class is not user-constructible.** `new Generator()` throws `Error`, and `clone` on a generator throws `Error` as well; the only way to obtain one is to call a generator function. - **Return type declarations are restricted.** A generator function may declare `Generator`, `Iterator`, `Traversable`, `iterable`, `object` or `mixed`; anything that is not a supertype of `Generator` (for example `array`) is a compile error. ## Why this matters in an interview The screening version of this question checks one idea: a generator is a **lazy producer**. The body is not a normal function that happens to be slow; it is code that runs in slices, paused at each `yield`, driven by whoever consumes it. Candidates who explain the call-returns-an-object step and the run-to-next-yield step can reason about every other generator behaviour from there.

  • How would you make a generator's argument validation fail at the call site instead of at the first iteration?
    Split it into two functions: a plain function that validates its arguments and then returns the result of calling a private generator function. The plain function runs eagerly, so the exception surfaces where the bad value was passed, and callers still receive a lazy `Generator`.
  • What return types may a PHP generator function declare?
    Any supertype of `Generator`: `Generator` itself, `Iterator`, `Traversable`, `iterable`, `object` or `mixed`. Declaring something else, such as `array`, is a compile error saying the generator return type must be a supertype of Generator. `Generator` is the most precise choice when callers use `send()` or `getReturn()`.

saying these in an interview costs you the question

  • Calling a generator function runs its body up to the first yield.
  • A generator function returns an array that is filled in the background.
  • yield ends the function the way return does.
  • Parameter type errors of a generator surface only at the first iteration.
  • You can create a generator directly with new Generator().