Generators & yield
debt(d5/e3/b3/t5)
Closest to 'specialist tool catches it' (d5). The detection_hints list phpstan and rector as tools, and the code_pattern describes a specific anti-pattern (building $results[] in a loop then returning) that these tools can flag. This is not caught by the compiler or a default linter, but requires a specialist static analysis or refactoring tool to identify.
Closest to 'simple parameterised fix' (e3). The quick_fix says to replace functions that build and return large arrays with generators using yield. This is a localised refactor — converting a function's return pattern to yield — typically within one function or one file, but not a single one-line swap since iteration call sites and try/finally cleanup may need minor adjustment.
Closest to 'localised tax' (b3). Applies to web, cli, and queue-worker contexts broadly, but the structural commitment is localised to the functions/components that consume large sequences. The rest of the codebase is unaffected. Callers do need to iterate rather than index into a collection, but this is a contained adaptation rather than a system-wide architectural constraint.
Closest to 'notable trap' (t5). The misconception field explicitly states that developers believe generators are 'just a fancy way to return arrays,' when in fact they are lazy and single-use. Common mistakes reinforce this: forgetting forward-only/single-use nature, misunderstanding when side effects run (yield pauses execution), and confusing return vs yield semantics. These are documented gotchas that most developers learn through experience, placing this squarely at t5.
Also Known As
TL;DR
Explanation
A generator function uses yield to produce values one at a time, pausing execution between yields. The caller receives a Generator object implementing the Iterator interface. This enables processing of large files, database result sets, or infinite sequences with O(1) memory usage rather than O(n). yield from enables delegation to another generator or iterable. Generators also support bidirectional communication — callers can send values back to the generator via Generator::send().
Diagram
flowchart LR
CALL2[Call generator function] --> ITER2[Returns Generator object<br/>body NOT executed]
ITER2 -->|foreach or next| RUN[Execute until yield]
RUN -->|yield value| SUSPENDED[Suspended<br/>value returned to caller]
SUSPENDED -->|next iteration| RUN
RUN -->|function ends| COMPLETE[Generator complete<br/>valid = false]
subgraph Memory_Benefit
EAGER[range 1 1000000<br/>builds full array in memory]
LAZY[xrange generator<br/>one value at a time]
EAGER --> MEM_BAD[400MB RAM]
LAZY --> MEM_GOOD[Constant memory]
end
style SUSPENDED fill:#6e40c9,color:#fff
style MEM_BAD fill:#f85149,color:#fff
style MEM_GOOD fill:#238636,color:#fff
Common Misconception
Why It Matters
Common Mistakes
- Using a generator where you need to rewind or count — generators are forward-only and single-use.
- Forgetting that yield pauses execution — side effects before yield do not run until the generator is iterated.
- Returning a value from a generator with return instead of yield — it sets the return value, not a yielded item.
- Not wrapping database cursor generators in try/finally to ensure the connection is closed on early exit.
Code Examples
// Loading all rows into memory:
function getAllUsers(): array {
return $this->db->query('SELECT * FROM users')->fetchAll(); // 1M rows in RAM
}
// Generator — yields one at a time:
function streamUsers(): Generator {
$stmt = $this->db->query('SELECT * FROM users');
while ($row = $stmt->fetch()) yield $row;
}
// Process large CSV without loading it all into memory
function readCsv(string $path): Generator {
$fp = fopen($path, 'r');
try {
while (($row = fgetcsv($fp)) !== false) {
yield $row;
}
} finally {
fclose($fp);
}
}
foreach (readCsv('million_rows.csv') as $row) {
processRow($row); // constant memory — one row at a time
}
// Infinite sequence
function fibonacci(): Generator {
[$a, $b] = [0, 1];
while (true) {
yield $a;
[$a, $b] = [$b, $a + $b];
}
}
$fib = fibonacci();
echo $fib->current(); // 0
$fib->next();
echo $fib->current(); // 1