ZetCode

PHP Generators

last modified May 21, 2025

In this article, we explore PHP generators, a powerful feature that allows for efficient iteration over large datasets while minimizing memory usage.

Generators provide a lightweight alternative to traditional iterators by yielding values one at a time, rather than storing entire collections in memory. Introduced in PHP 5.5, they allow developers to use foreach loops on large data sources without needing to construct full arrays, making them ideal for handling large files, database records, and API results efficiently.

The advantages of PHP generators are:

By using generators, developers can optimize performance and memory usage when dealing with large datasets, making them an essential tool for efficient data handling in PHP.

Basic generator syntax

Generators are functions that use the yield keyword instead of return. When called, they return a Generator object that can be iterated over. Each yield provides one value to the iteration.

basic_generator.php
<?php

declare(strict_types=1);

function numberGenerator(int $start, int $end): Generator {
    for ($i = $start; $i <= $end; $i++) {
        yield $i;
    }
}

// Using the generator
foreach (numberGenerator(1, 5) as $number) {
    echo "$number\n";
}

// Generator returns a Generator object
$generator = numberGenerator(1, 3);
echo get_class($generator) . "\n"; // Generator

// Manual iteration
$generator = numberGenerator(10, 12);
echo $generator->current() . "\n"; // 10
$generator->next();
echo $generator->current() . "\n"; // 11

This shows a basic generator that yields numbers in a range. The generator preserves its state between yields, allowing it to resume where it left off. Generators are interruptible functions that can be paused and resumed.

λ php basic_generator.php
1
2
3
4
5
Generator
10
11

Memory efficiency

Generators are memory-efficient because they only generate one value at a time, unlike arrays which must store all values in memory. This makes them ideal for working with large datasets.

memory_efficiency.php
<?php

declare(strict_types=1);

// Memory intensive approach
function getLinesFromFile(string $filename): array {
    $lines = [];
    $file = fopen($filename, 'r');
    
    while (!feof($file)) {
        $lines[] = trim(fgets($file));
    }
    
    fclose($file);
    return $lines;
}

// Generator approach
function getLinesFromFileGenerator(string $filename): Generator {
    $file = fopen($filename, 'r');
    
    while (!feof($file)) {
        yield trim(fgets($file));
    }
    
    fclose($file);
}

// Memory usage comparison
$filename = 'large_file.txt';

// Array approach
$memoryBefore = memory_get_usage();
$lines = getLinesFromFile($filename);
$memoryAfter = memory_get_usage();
echo "Array memory: " . ($memoryAfter - $memoryBefore) . " bytes\n";

// Generator approach
$memoryBefore = memory_get_usage();
$linesGenerator = getLinesFromFileGenerator($filename);
$memoryAfter = memory_get_usage();
echo "Generator memory: " . ($memoryAfter - $memoryBefore) . " bytes\n";

// Process large file without loading it all
foreach (getLinesFromFileGenerator($filename) as $line) {
    // Process each line without memory issues
    if (str_contains($line, 'error')) {
        echo "Found error in line: $line\n";
    }
}

The generator version uses significantly less memory because it only holds one line in memory at a time. This difference becomes crucial when working with files that are too large to fit in memory all at once.

λ php memory_efficiency.php
Array memory: 10485760 bytes
Generator memory: 416 bytes
Found error in line: Sample error line 1
Found error in line: Sample error line 2

Keyed yields

Generators can yield key-value pairs, making them suitable for generating associative sequences. This is done by yielding an array or using the yield key => value syntax.

keyed_yields.php
<?php

declare(strict_types=1);

function assocGenerator(): Generator {
    yield 'name' => 'Alice';
    yield 'age' => 30;
    yield 'occupation' => 'Developer';
}

foreach (assocGenerator() as $key => $value) {
    echo "$key: $value\n";
}

// More complex example
function csvGenerator(string $filename): Generator {
    $file = fopen($filename, 'r');
    
    // Get headers
    $headers = fgetcsv($file);
    
    while ($row = fgetcsv($file)) {
        yield array_combine($headers, $row);
    }
    
    fclose($file);
}

// Process CSV file
foreach (csvGenerator('data.csv') as $record) {
    echo "Processing: {$record['name']} ({$record['email']})\n";
}

// Numeric keys
function indexedGenerator(): Generator {
    for ($i = 0; $i < 5; $i++) {
        yield $i => "Item $i";
    }
}

foreach (indexedGenerator() as $index => $item) {
    echo "$index: $item\n";
}

This demonstrates how generators can produce associative arrays and handle structured data like CSV files. The key-value pairs are yielded directly and can be used in foreach loops just like regular associative arrays.

λ php keyed_yields.php
name: Alice
age: 30
occupation: Developer
Processing: John Doe (john@example.com)
Processing: Jane Smith (jane@example.com)
0: Item 0
1: Item 1
2: Item 2
3: Item 3
4: Item 4

Generator delegation

PHP 7.0 introduced generator delegation using yield from, which allows a generator to yield values from another generator, array, or Traversable object. This enables generator composition and cleaner code.

generator_delegation.php
<?php

declare(strict_types=1);

function countTo3(): Generator {
    yield 1;
    yield 2;
    yield 3;
}

function countTo6(): Generator {
    yield from countTo3();
    yield 4;
    yield 5;
    yield 6;
}

foreach (countTo6() as $number) {
    echo "$number ";
}
echo "\n";

// Yield from array
function yieldArray(): Generator {
    yield from ['a', 'b', 'c'];
}

foreach (yieldArray() as $letter) {
    echo "$letter ";
}
echo "\n";

// Complex delegation
function readMultipleFiles(array $filenames): Generator {
    foreach ($filenames as $filename) {
        yield from readFileLines($filename);
    }
}

function readFileLines(string $filename): Generator {
    $file = fopen($filename, 'r');
    
    while (!feof($file)) {
        yield trim(fgets($file));
    }
    
    fclose($file);
}

// Process multiple files as one sequence
foreach (readMultipleFiles(['file1.txt', 'file2.txt']) as $line) {
    echo "Line: $line\n";
}

// Yield from with keys
function combinedGenerator(): Generator {
    yield from ['a' => 1, 'b' => 2];
    yield from ['c' => 3];
    yield 'd' => 4;
}

foreach (combinedGenerator() as $k => $v) {
    echo "$k:$v ";
}
echo "\n";

Generator delegation simplifies working with multiple generators by allowing them to be combined seamlessly. The yield from syntax handles all the iteration internally, making the code cleaner and more modular.

λ php generator_delegation.php
1 2 3 4 5 6 
a b c 
Line: First line of file1
Line: Second line of file1
Line: First line of file2
a:1 b:2 c:3 d:4 

Generator methods

Generator objects provide several methods for more advanced control over iteration. These include send, throw, and getReturn for two-way communication with the generator.

generator_methods.php
<?php

declare(strict_types=1);

function interactiveGenerator(): Generator {
    $value = yield 'first';
    echo "Received: $value\n";
    
    $value = yield 'second';
    echo "Received: $value\n";
    
    return 'done';
}

$gen = interactiveGenerator();

// Get first yielded value
echo "Yielded: " . $gen->current() . "\n";

// Send value to generator
$gen->send('hello');

// Get next yielded value
echo "Yielded: " . $gen->current() . "\n";

// Send another value
$gen->send('world');

// Get return value
try {
    $gen->next();
    echo "Returned: " . $gen->getReturn() . "\n";
} catch (Exception $e) {
    echo "Exception: " . $e->getMessage() . "\n";
}

// Exception handling in generators
function failingGenerator(): Generator {
    try {
        yield 'start';
        throw new Exception('Generator error');
    } catch (Exception $e) {
        yield 'caught: ' . $e->getMessage();
    }
    
    yield 'end';
}

$gen = failingGenerator();
echo $gen->current() . "\n";
$gen->next();
echo $gen->current() . "\n";
$gen->next();
echo $gen->current() . "\n";

This demonstrates two-way communication with generators. Values can be sent into the generator using send(), and exceptions can be thrown into it with throw(). The return value is accessed via getReturn() after iteration completes.

λ php generator_methods.php
Yielded: first
Received: hello
Yielded: second
Received: world
Returned: done
start
caught: Generator error
end

Real-world use cases

Generators are useful in many practical scenarios where memory efficiency or lazy evaluation is important. Here are some common real-world examples.

real_world.php
<?php

declare(strict_types=1);

// 1. Processing large files
function processLargeFile(string $filename): Generator {
    $file = fopen($filename, 'r');
    
    while (!feof($file)) {
        $line = fgets($file);
        yield json_decode($line, true);
    }
    
    fclose($file);
}

// 2. Paginating database results
function paginateResults(PDO $pdo, string $query, int $pageSize = 100): Generator {
    $offset = 0;
    
    do {
        $stmt = $pdo->prepare($query . " LIMIT ? OFFSET ?");
        $stmt->execute([$pageSize, $offset]);
        $results = $stmt->fetchAll();
        
        yield from $results;
        
        $offset += $pageSize;
    } while (count($results) === $pageSize);
}

// 3. Generating infinite sequences
function fibonacciSequence(): Generator {
    $a = 0;
    $b = 1;
    
    while (true) {
        yield $a;
        [$a, $b] = [$b, $a + $b];
    }
}

$fib = fibonacciSequence();
for ($i = 0; $i < 10; $i++) {
    echo $fib->current() . " ";
    $fib->next();
}
echo "\n";

// 4. Processing API responses
function fetchPaginatedAPI(string $baseUrl): Generator {
    $url = $baseUrl;
    
    do {
        $response = json_decode(file_get_contents($url), true);
        yield from $response['data'];
        
        $url = $response['next_page'] ?? null;
    } while ($url);
}

// 5. Data transformation pipeline
function transformData(iterable $source, callable ...$transformers): Generator {
    foreach ($source as $item) {
        $result = $item;
        
        foreach ($transformers as $transformer) {
            $result = $transformer($result);
        }
        
        yield $result;
    }
}

// Example pipeline
$data = [' apple ', ' banana ', ' cherry '];
$pipeline = transformData(
    $data,
    fn($s) => trim($s),
    fn($s) => strtoupper($s),
    fn($s) => "FRUIT: $s"
);

foreach ($pipeline as $item) {
    echo "$item\n";
}

These examples show generators solving real problems elegantly. From handling large datasets to creating processing pipelines, generators provide memory- efficient solutions that would be difficult with traditional arrays.

λ php real_world.php
0 1 1 2 3 5 8 13 21 34 
FRUIT: APPLE
FRUIT: BANANA
FRUIT: CHERRY

Best practices

Follow these best practices when working with generators to write clean, efficient, and maintainable code.

For small datasets, performance considerations are essential. In many cases, arrays offer better efficiency than generators, as they allow direct indexing and faster iteration without the overhead of maintaining generator state.

// Bad:
function getAllUsers(): array {
    $users = [];
    // Fetch all from DB
    return $users;
}

// Good:
function getAllUsersGenerator(): Generator {
    // Fetch one at a time
    while ($user = fetchUserFromDB()) {
        yield $user;
    }
}

Use generators for memory efficiency with large datasets

/** @return Generator<int, User, mixed, void> */
function getUserGenerator(): Generator {
    // ...
}

Document generator return types.

function combinedData(): Generator {
    yield from getUsers();
    yield from getProducts();
}

Use yield from for generator composition.

function readFileWithCleanup(string $filename): Generator {
    $file = fopen($filename, 'r');
    try {
        while (!feof($file)) {
            yield trim(fgets($file));
        }
    } finally {
        fclose($file);
    }
}

Clean up resources in generators.

$gen = someGenerator();
foreach ($gen as $value) {
    // ...
}
// Can't iterate again - generator is exhausted
// foreach ($gen as $value) {} // Won't work

Be aware that generators are forward-only. Once a generator has been exhausted, it cannot be reset or reused. If you need to iterate over the same values multiple times, consider storing them in an array or using a different generator.

function filter(iterable $items, callable $predicate): Generator {
    foreach ($items as $item) {
        if ($predicate($item)) {
            yield $item;
        }
    }
}

Use generators for lazy evaluation.

// Works well with match, arrow functions, etc.
function transformGenerator(iterable $items): Generator {
    foreach ($items as $item) {
        yield match(true) {
            is_int($item) => $item * 2,
            is_string($item) => strtoupper($item),
            default => $item
        };
    }
}

Use generators with other language features like match and arrow functions for cleaner code..

/**
 * @yield int The generated ID
 * @yield string The associated name
 */
function idNameGenerator(): Generator {
    // ...
}

Document expected yields for better code clarity.

These practices help ensure generators are used effectively. Key points include proper documentation, resource cleanup, and choosing generators for memory efficiency rather than convenience in all cases.

PHP generators provide a powerful tool for efficient iteration and working with large datasets. Key points to remember:

Generators are a versatile feature that can significantly improve memory usage and code clarity when working with sequences of data. They're particularly valuable in data processing pipelines and when dealing with large or infinite sequences.

Author

My name is Jan Bodnar, and I am a passionate programmer with extensive programming experience. I have been writing programming articles since 2007. To date, I have authored over 1,400 articles and 8 e-books. I possess more than ten years of experience in teaching programming.

List all PHP tutorials.