A standard PHP function that builds and returns an array loads the entire dataset into memory before the caller receives a single value. For small datasets this is fine. For reading a 500 MB log file, iterating through millions of database rows, or consuming a paginated API with hundreds of pages, the memory overhead becomes a real constraint.
Generators solve this by producing values one at a time rather than assembling them all upfront. The function pauses at each
yieldstatement, returns the current value to the caller, and resumes from the same point on the next iteration. The result is constant memory usage regardless of dataset size.What this covers:
How generators work and how they differ from regular functions
yieldvsreturnYielding key-value pairs
yield fromfor generator delegationReal-world examples: large file processing and paginated APIs
Practical considerations and limitations
How Generators Work
A generator is a function that contains at least one yield statement. When called, it does not execute immediately. Instead, it returns a Generator object, which implements the Iterator interface. The function body executes only when the caller requests the next value.
function numbers(): Generator {
yield 1;
yield 2;
yield 3;
}
foreach (numbers() as $num) {
echo $num . "\n"; // 1, then 2, then 3
}
Each foreach iteration resumes the function from the point after the last yield, executes until the next yield, and delivers that value. When the function returns (either explicitly or by reaching the end), the loop ends.
The memory advantage is direct: the function never holds more than one value at a time in the context of the generator itself.
yield vs return
return terminates the function and delivers a single value to the caller. Execution cannot resume.
yield suspends the function and delivers a value to the caller. Execution resumes from the suspension point when the next value is requested.
function withReturn(): array {
return [1, 2, 3]; // all three values allocated at once
}
function withYield(): Generator {
yield 1; // one value at a time
yield 2;
yield 3;
}
A generator function can include a return statement. The returned value is not yielded to the caller as part of the iteration, but it is accessible via $generator->getReturn() after the generator is exhausted. This is occasionally useful for returning a summary or status after processing is complete.
Yielding Key-Value Pairs
yield supports key-value syntax, which works like associative array iteration:
function countryCities(): Generator {
yield 'NG' => 'Lagos';
yield 'KE' => 'Nairobi';
yield 'GH' => 'Accra';
}
foreach (countryCities() as $code => $city) {
echo "$code: $city\n";
}
Without explicit keys, generators yield integer keys starting from zero, the same default as numeric arrays.
Delegating with yield from
PHP 7 introduced yield from, which delegates iteration to another generator, array, or any Traversable object. The delegated generator runs to completion before the outer generator continues.
function inner(): Generator {
yield 1;
yield 2;
}
function outer(): Generator {
yield from inner();
yield 3;
}
foreach (outer() as $val) {
echo $val . "\n"; // 1, 2, 3
}
yield from is most useful for composing generators from smaller, reusable parts, or for flattening nested iterables without building intermediate arrays.
When delegating to another generator, the return value of the inner generator is returned to the outer generator as the result of the yield from expression:
function inner(): Generator {
yield 1;
return 'done';
}
function outer(): Generator {
$result = yield from inner();
echo $result; // "done"
}
Real-World Example: Processing a Large File
Reading an entire large file into memory with file() is a common source of memory exhaustion in PHP applications:
// Loads the entire file into memory
$lines = file('huge_file.txt');
foreach ($lines as $line) {
processLine($line);
}
A generator reads line by line, keeping memory usage constant regardless of file size:
function readLines(string $filePath): Generator {
$handle = fopen($filePath, 'r');
if ($handle === false) {
throw new RuntimeException("Cannot open file: $filePath");
}
try {
while (($line = fgets($handle)) !== false) {
yield $line;
}
} finally {
fclose($handle);
}
}
foreach (readLines('huge_file.txt') as $line) {
processLine($line);
}
The finally block guarantees the file handle is closed whether or not an exception is thrown during processing. Without this, an exception in processLine() would leave the handle open.
Real-World Example: Paginated API Responses
Fetching all pages of a paginated API into an array before processing requires memory proportional to the total dataset. A generator fetches and yields each page on demand:
function fetchPages(string $baseUrl, int $totalPages): Generator {
for ($page = 1; $page <= $totalPages; $page++) {
$url = $baseUrl . "?page=$page";
$response = file_get_contents($url);
if ($response === false) {
throw new RuntimeException("Failed to fetch page $page from $url");
}
$data = json_decode($response, true);
yield from $data['results'];
}
}
foreach (fetchPages('https://api.example.com/data', 5) as $item) {
processItem($item);
}
Using yield from $data['results'] flattens the results array so each individual item is yielded rather than yielding each page as an array. The calling code processes items one at a time without needing to know about the pagination structure.
Practical Considerations
Generators are forward-only. Unlike arrays, a generator cannot be rewound after iteration begins. If the dataset needs to be iterated multiple times, either call the generator function again (re-running the logic) or collect results into an array first.
Error handling inside generators. Exceptions thrown inside a generator propagate to the caller at the point where current(), next(), or the foreach loop resumes the generator. Wrap generator usage in try/catch blocks in the calling code, and use finally inside the generator for resource cleanup.
Sending values into a generator. PHP generators support bidirectional communication via $generator->send($value). The sent value becomes the result of the yield expression inside the generator. This enables coroutine-style patterns, though it is less commonly needed in application code.
Type declarations. Generator functions can be declared with a return type of Generator or iterable. Using Generator is more precize if the caller needs access to generator-specific methods like send() or getReturn(). Using iterable is more flexible if the implementation might change.
Key Takeaways
Generators yield values one at a time rather than loading all values into memory. Memory usage stays constant regardless of dataset size.
yieldsuspends a function and delivers a value to the caller. The function resumes from the same point on the next iteration.yield key => valueworks like associative array iteration. Without explicit keys, generators yield integer keys from zero.yield fromdelegates to another generator, array, orTraversable. The inner generator runs to completion before the outer generator continues.Always use
finallyinside generators that open resources to guarantee cleanup even when exceptions are thrown during iteration.Generators are forward-only. Rewinding requires calling the generator function again.
Conclusion
Generators are the correct tool for any PHP code that processes data larger than comfortably fits in memory, reads from streams, or consumes paginated sources where loading all results upfront is impractical. The yield keyword is syntactically simple but changes the execution model meaningfully: lazy evaluation, constant memory usage, and clean separation between data production and data consumption.
The file reading and paginated API examples here represent the two most common application patterns. Both follow the same structure: a generator function that encapsulates the iteration logic, and calling code that consumes values through a standard foreach loop without knowing or caring how the values are produced.
Using generators in a specific context and running into a limitation? Describe the scenario in the comments.




