Understanding PHP Generators and the yield Keyword

Understanding PHP Generators and the yield Keyword

Sunday, July 12, 2026

Learn how PHP Generators work and master the yield keyword with practical examples. Improve memory usage, process large datasets efficiently, and understand when generators are the right solution.

Table of Contents

When working with collections in PHP, the most common approach is to build an array and return it. While this works well for small datasets, it can quickly become inefficient when processing thousands—or even millions—of records.

PHP Generators solve this problem by allowing values to be produced one at a time instead of storing everything in memory. They provide a simple and elegant way to implement lazy evaluation, making applications faster and significantly more memory-efficient.

In this article, you'll learn how Generators work, what the yield keyword actually does, when to use them, their limitations, and several real-world examples that you can apply immediately.


1. What Are PHP Generators?

A Generator is a special type of function that allows PHP to produce values one at a time instead of building and returning an entire collection at once. Unlike a regular function that returns a value only once, a generator can pause its execution, yield a value, and later resume exactly where it left off.

Generators are created using the yield keyword. As soon as PHP encounters yield, it temporarily suspends the function, preserving all local variables and the current execution state. When the next value is requested, the function continues from the exact point where it stopped.

This behavior is known as lazy evaluation (or lazy loading), because values are generated only when they are actually needed.

A regular function typically returns an array containing every value:

function numbers(): array
{
    return [1, 2, 3];
}

A generator produces each value individually:

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

Although both functions can be consumed with foreach, they behave very differently behind the scenes. An array must already exist in memory before iteration begins, while a generator creates each value on demand.

This makes generators particularly useful when working with:

  • Large datasets
  • Database records
  • CSV files
  • Log files
  • Paginated APIs
  • Infinite sequences
  • Streaming data

Instead of allocating memory for every element, a generator only keeps the current execution state in memory, making it an efficient solution for sequential data processing.


2. The Problem with Arrays

Arrays are one of the most commonly used data structures in PHP. They are flexible, easy to use, and provide fast random access to elements. However, they also have an important limitation: every element must exist in memory before the array can be returned.

Consider the following function:

function numbers(): array
{
    $result = [];

    for ($i = 1; $i <= 1_000_000; $i++) {
        $result[] = $i;
    }

    return $result;
}

Using it is straightforward:

foreach (numbers() as $number) {
    // Process each number...
}

The problem is that PHP cannot start the foreach until the entire array has been built.

Internally, the process looks like this:

  1. Allocate memory for an empty array.
  2. Create one million integers.
  3. Store all of them in the array.
  4. Return the completed array.
  5. Finally start iterating over it.

Even if your code only needs the first few values, PHP has already spent time and memory generating every single element.

For small collections, this is rarely a concern. However, imagine processing:

  • a CSV file containing 20 million rows;
  • a database table with several million records;
  • an API returning thousands of pages of results;
  • years of application logs.

Loading everything into an array can dramatically increase memory usage and may even exceed PHP's memory_limit.

The larger the dataset becomes, the more memory the application consumes. Eventually, this can lead to slower execution or fatal errors such as:

Allowed memory size exhausted

In situations like these, creating the entire collection upfront is unnecessary. Most applications process items sequentially, one after another, making it much more efficient to generate values only when they are needed.

This is exactly the problem that Generators were designed to solve.


3. Introducing yield

Generators are created using the yield keyword. At first glance, it may look similar to return, but its behavior is fundamentally different.

A regular function finishes its execution as soon as it reaches return:

function numbers(): array
{
    return [1, 2, 3];
}

A generator, on the other hand, can produce multiple values during a single execution:

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

Consuming the generator looks almost identical to iterating over an array:

foreach (numbers() as $number) {
    echo $number . PHP_EOL;
}

Output:

1
2
3

Despite the similar syntax, the function does not return an array.

Instead, it returns an instance of PHP's built-in Generator class:

$generator = numbers();

var_dump($generator);

Output:

object(Generator)

Each time the foreach requests the next value, the generator resumes execution until it encounters the next yield.

You can think of yield as saying:

"Here's the next value. Pause the function here, and continue from this exact point when the next value is requested."

This behavior allows PHP to process data incrementally instead of building an entire collection first.

A generator can also be mixed with normal PHP code:

function countdown(int $from): Generator
{
    while ($from > 0) {
        echo "Generating {$from}..." . PHP_EOL;

        yield $from;

        $from--;
    }
}

Notice that the message is printed only when the next value is requested:

foreach (countdown(3) as $number) {
    echo "Received {$number}" . PHP_EOL;
}

Output:

Generating 3...
Received 3
Generating 2...
Received 2
Generating 1...
Received 1

This demonstrates one of the defining characteristics of generators: execution happens progressively, not all at once. Instead of producing an entire collection upfront, each value is generated only when the consumer asks for it, making generators an excellent choice for processing large or potentially endless streams of data.


4. How Generators Work Internally

One of the most interesting aspects of generators is that they don't execute the entire function at once. Instead, PHP pauses the function every time it encounters a yield statement and remembers its current state.

That means all local variables, the current instruction pointer, and the execution context are preserved. When the next value is requested, the function resumes exactly where it stopped.

Consider the following example:

function demo(): Generator
{
    echo "Step A" . PHP_EOL;
    yield 1;

    echo "Step B" . PHP_EOL;
    yield 2;

    echo "Step C" . PHP_EOL;
}

Now iterate over the generator:

foreach (demo() as $value) {
    echo "Received: {$value}" . PHP_EOL;
}

Output:

Step A
Received: 1
Step B
Received: 2
Step C

Notice what happens:

  1. The function starts executing.
  2. "Step A" is printed.
  3. yield 1 returns the first value.
  4. The function pauses.
  5. The foreach receives the value.
  6. PHP resumes execution after the first yield.
  7. "Step B" is printed.
  8. yield 2 returns the second value.
  9. The function pauses again.
  10. After the last value is consumed, execution continues until the function ends.

This is fundamentally different from a regular function. A normal function executes from beginning to end without interruption before returning control to the caller.

Generators behave more like state machines: they pause, preserve their state, and continue later.

Because only the current execution state is stored, generators consume very little memory regardless of how many values they can potentially produce.


5. Generating Infinite Sequences

One of the biggest advantages of generators is that they can represent infinite sequences.

An array can never contain an infinite number of elements because it must allocate memory for every value. A generator, however, produces values on demand, so it doesn't need to know where the sequence ends.

Here's a simple counter:

function counter(): Generator
{
    $i = 1;

    while (true) {
        yield $i++;
    }
}

Although this generator never stops by itself, consuming it safely is easy:

$count = 0;

foreach (counter() as $number) {
    echo $number . PHP_EOL;

    if (++$count === 10) {
        break;
    }
}

Output:

1
2
3
4
5
6
7
8
9
10

The generator could continue producing numbers forever, but the consumer decides when to stop.

This pattern is extremely useful for applications that work with continuous streams of data, such as:

  • Event processing
  • Message queues
  • Sensor data
  • Network packets
  • Live log monitoring
  • Random number generation
  • Pagination without a predefined end

You can even generate more complex infinite sequences.

For example, the Fibonacci sequence:

function fibonacci(): Generator
{
    $a = 0;
    $b = 1;

    while (true) {
        yield $a;

        [$a, $b] = [$b, $a + $b];
    }
}

Using only the first fifteen values:

$count = 0;

foreach (fibonacci() as $number) {
    echo $number . PHP_EOL;

    if (++$count === 15) {
        break;
    }
}

The generator is theoretically infinite, but memory usage remains almost constant because it stores only the current state ($a and $b), not every previously generated number.


6. Reading Large Files Efficiently

One of the most common real-world uses for generators is reading very large files.

Suppose you have a log file containing several gigabytes of data. A naive approach would be to load the entire file into memory:

$lines = file('application.log');

foreach ($lines as $line) {
    // Process each line...
}

While this is convenient, file() reads every line before your loop even begins. For sufficiently large files, this can consume hundreds of megabytes—or even gigabytes—of memory.

A generator offers a much more scalable solution:

function readLines(string $filename): Generator
{
    $handle = fopen($filename, 'r');

    if ($handle === false) {
        throw new RuntimeException("Unable to open file.");
    }

    try {
        while (($line = fgets($handle)) !== false) {
            yield rtrim($line, "\r\n");
        }
    } finally {
        fclose($handle);
    }
}

Using it is just as simple:

foreach (readLines('application.log') as $line) {
    echo $line . PHP_EOL;
}

The important difference is that only one line at a time is kept in memory.

The execution flow is roughly:

  1. Open the file.
  2. Read the first line.
  3. Yield the line.
  4. Wait until the consumer requests another value.
  5. Read the next line.
  6. Repeat until the end of the file.
  7. Close the file.

This approach scales to files of virtually any size because memory consumption remains almost constant.

Generators are also an excellent fit for processing:

  • CSV imports
  • XML exports
  • JSON Lines (JSONL) files
  • Apache or Nginx access logs
  • Backup files
  • Database dumps
  • Streaming data from external services

Whenever data can be processed sequentially, a generator is often a better choice than loading the entire dataset into memory first.


7. Using yield with Keys

Just like arrays, generators can produce key-value pairs. This makes them compatible with foreach, allowing you to access both the key and the value during iteration.

Instead of yielding only a value:

yield 'Alice';

you can explicitly specify a key:

function getUser(): Generator
{
    yield 'id' => 42;
    yield 'name' => 'Alice';
    yield 'email' => 'alice@example.com';
}

Iterating over the generator works exactly as it would with an associative array:

foreach (getUser() as $key => $value) {
    echo "{$key}: {$value}" . PHP_EOL;
}

Output:

id: 42
name: Alice
email: alice@example.com

Generators can also produce numeric keys.

If no key is provided, PHP assigns sequential integer keys automatically:

function colors(): Generator
{
    yield 'Red';
    yield 'Green';
    yield 'Blue';
}

Which is equivalent to:

function colors(): Generator
{
    yield 0 => 'Red';
    yield 1 => 'Green';
    yield 2 => 'Blue';
}

You are also free to define custom numeric keys:

function statusCodes(): Generator
{
    yield 200 => 'OK';
    yield 404 => 'Not Found';
    yield 500 => 'Internal Server Error';
}

This can be useful when streaming structured data where the key itself carries meaning, such as:

  • HTTP status codes
  • Product IDs
  • Database primary keys
  • Configuration options
  • Language codes

Although generators support keys, keep in mind that they are still iterators, not arrays. You cannot access a yielded value directly by its key (for example, $generator[404] is not supported). Keys are available only during iteration.


8. Delegating with yield from

As applications grow, generators often need to be composed from smaller generators. Instead of manually looping over one generator and yielding each value yourself, PHP provides the yield from syntax.

Suppose you have separate generators for different groups of log messages:

function infoLogs(): Generator
{
    yield '[INFO] Application started';
    yield '[INFO] User authenticated';
}

function errorLogs(): Generator
{
    yield '[ERROR] Database connection failed';
    yield '[ERROR] Retry limit exceeded';
}

Without yield from, combining them would require another loop:

function allLogs(): Generator
{
    foreach (infoLogs() as $message) {
        yield $message;
    }

    foreach (errorLogs() as $message) {
        yield $message;
    }
}

This works, but it's unnecessarily verbose.

Using yield from, the same generator becomes much cleaner:

function allLogs(): Generator
{
    yield from infoLogs();
    yield from errorLogs();
}

The result is identical:

foreach (allLogs() as $message) {
    echo $message . PHP_EOL;
}

Output:

[INFO] Application started
[INFO] User authenticated
[ERROR] Database connection failed
[ERROR] Retry limit exceeded

The delegated generator behaves as if its values had been yielded directly by the outer generator.

yield from isn't limited to generators—it also works with any iterable object, including arrays.

function configuration(): Generator
{
    yield 'Application Configuration';

    yield from [
        'Environment: Production',
        'Cache: Enabled',
        'Debug: Disabled',
    ];

    yield 'Configuration Loaded';
}

Using yield from generally leads to code that is:

  • Shorter
  • Easier to read
  • Easier to maintain
  • Less error-prone

Whenever your generator's only purpose is to forward values from another iterable, yield from is usually the preferred solution.


9. Generator vs Array

Generators and arrays are both iterable, but they serve different purposes. Choosing the right one depends on how your data will be used.

The following table summarizes their main differences:

Feature Generator Array
Memory usage Very low Grows with the number of elements
Lazy evaluation ✅ Yes ❌ No
Random access ($items[5]) ❌ No ✅ Yes
Can be reused ❌ No (after exhaustion) ✅ Yes
Supports count() directly ❌ No ✅ Yes
Suitable for infinite sequences ✅ Yes ❌ No
Sequential processing ✅ Excellent ✅ Good
Best for large datasets ✅ Yes ⚠️ Depends on available memory

Memory Consumption

Arrays allocate memory for every element they contain.

Generators only keep the current execution state in memory, regardless of how many values they can eventually produce.

For example, processing one million database records with an array requires storing every record before iteration begins. A generator can process one record, discard it, and move on to the next, keeping memory usage nearly constant.

Random Access

Arrays excel when you need to access arbitrary elements.

$products = [
    'Laptop',
    'Keyboard',
    'Mouse',
];

echo $products[1];

Generators do not support this.

Because values are generated on demand, they don't exist until the iterator reaches them.

Reusability

Arrays can be iterated multiple times:

foreach ($products as $product) {
    // ...
}

foreach ($products as $product) {
    // Iterate again
}

Generators are consumed only once.

After reaching the end, they cannot simply be restarted. To iterate again, you must create a new generator instance by calling the generator function again.

Performance

A common misconception is that generators are always faster than arrays.

In reality, their primary advantage is reduced memory consumption, not raw execution speed.

Since generators produce one value at a time, they introduce a small amount of overhead compared to iterating over an array that is already in memory. However, this overhead is often negligible and is outweighed by the ability to process datasets that would otherwise exhaust available memory.

When to Use Each

Use a Generator when:

  • Processing very large datasets
  • Reading files line by line
  • Streaming data from APIs
  • Working with paginated results
  • Building pipelines
  • Producing values lazily
  • Generating potentially infinite sequences

Use an Array when:

  • The dataset is reasonably small
  • You need random access
  • You need to sort, filter, or modify the collection repeatedly
  • You need functions like count(), array_map(), or array_filter()
  • The collection must be reused multiple times

Neither structure is universally better. Arrays remain the most practical choice for many everyday tasks, while generators shine when memory efficiency and sequential processing become the primary concern.


10. Common Use Cases

Although generators can be used anywhere an iterable is expected, they are particularly valuable when data can be processed sequentially without keeping the entire dataset in memory.

Here are some of the most common real-world use cases.

Processing Large Database Result Sets

When querying a large table, it's often unnecessary to load every record into an array before processing them.

Instead, records can be yielded one at a time:

function users(PDO $pdo): Generator
{
    $statement = $pdo->query('SELECT id, name FROM users');

    while ($user = $statement->fetch(PDO::FETCH_ASSOC)) {
        yield $user;
    }
}

This allows your application to process millions of rows while keeping memory usage low.


Paginated APIs

Many REST APIs return data in multiple pages.

A generator can hide the pagination logic from the consumer:

function fetchRepositories(ApiClient $client): Generator
{
    $page = 1;

    while ($repositories = $client->repositories($page++)) {
        foreach ($repositories as $repository) {
            yield $repository;
        }
    }
}

The caller doesn't need to know how many pages exist—it simply iterates over the results.


Importing CSV Files

Instead of loading an entire CSV into memory, each row can be processed as soon as it's read.

function readCsv(string $filename): Generator
{
    $handle = fopen($filename, 'r');

    try {
        while (($row = fgetcsv($handle)) !== false) {
            yield $row;
        }
    } finally {
        fclose($handle);
    }
}

This approach scales well even for files containing millions of records.


Data Transformation Pipelines

Generators are excellent for building pipelines where each step transforms data before passing it to the next.

function uppercase(iterable $names): Generator
{
    foreach ($names as $name) {
        yield strtoupper($name);
    }
}

Because values are transformed one at a time, no intermediate arrays need to be created.


Streaming External Data

Some services expose data continuously instead of returning a complete response.

Generators naturally fit these scenarios because they can yield values as soon as they become available.

Examples include:

  • Message queues
  • WebSocket messages
  • Event streams
  • Live telemetry
  • Log monitoring

Lazy Data Generation

Sometimes data doesn't exist yet—it can simply be computed whenever requested.

Examples include:

  • UUID generation
  • Random values
  • Date ranges
  • Password candidates
  • Mathematical sequences

Rather than generating thousands of values upfront, a generator creates each one only when it's actually needed.


11. Limitations

While generators are powerful, they are not a replacement for arrays. Understanding their limitations is just as important as understanding their strengths.

No Random Access

Generators can only move forward.

Unlike arrays, you cannot retrieve an arbitrary element:

$generator[10];

This is not supported because previous values are not stored.


Single Iteration

A generator is consumed as it is iterated.

Once it reaches the end, it cannot simply be reused:

$generator = getItems();

foreach ($generator as $item) {
    // ...
}

foreach ($generator as $item) {
    // Will not work as expected
}

If you need to iterate again, create a new generator by calling the function again.


Cannot Be Counted Directly

Arrays work naturally with count():

count($items);

Generators do not.

Since values haven't been generated yet, PHP has no way of knowing how many there will be without consuming the entire iterator.


Sequential Processing Only

Generators are designed for forward-only iteration.

If your algorithm frequently needs to:

  • jump to arbitrary positions,
  • sort data,
  • shuffle elements,
  • search repeatedly,
  • compare distant elements,

an array or another data structure will usually be a better choice.


Values Are Not Cached

Every yielded value exists only for the current iteration.

If generating each value is computationally expensive and you'll need it again later, repeatedly recreating the generator may be slower than storing the results in an array.

In other words, generators trade memory efficiency for the ability to recompute values when needed.


12. Best Practices

Generators are most effective when used intentionally. The following practices help keep your code efficient and easy to understand.

Use Generators for Large or Streaming Data

Generators provide the greatest benefit when working with:

  • Large files
  • Database cursors
  • Paginated APIs
  • Continuous streams
  • Expensive computations

For a collection of only a few dozen items, an array is usually simpler and perfectly adequate.


Prefer iterable in Public APIs

If a function only needs to iterate over incoming data, type-hinting iterable makes it more flexible.

function export(iterable $records): void
{
    foreach ($records as $record) {
        // ...
    }
}

The caller can provide either an array or a generator without changing the implementation.


Use yield from to Compose Generators

Whenever you're forwarding values from another iterable, prefer yield from over manually writing nested foreach loops.

Besides reducing boilerplate, it makes the intent of the code much clearer.


Keep Generators Focused

A generator should ideally have a single responsibility.

For example:

  • Read records
  • Transform records
  • Filter records
  • Export records

Instead of creating one large generator that performs every operation, compose multiple small generators. This makes the code easier to test, reuse, and maintain.


Release External Resources Properly

Generators frequently work with files, sockets, or database cursors.

Whenever external resources are involved, ensure they are released even if iteration stops unexpectedly.

Using try/finally is a simple and reliable approach:

function lines(string $filename): Generator
{
    $handle = fopen($filename, 'r');

    try {
        while (($line = fgets($handle)) !== false) {
            yield $line;
        }
    } finally {
        fclose($handle);
    }
}

This guarantees that the file handle is closed regardless of how the generator terminates.


Don't Use Generators Prematurely

Generators are a powerful optimization tool, but they shouldn't be used simply because they exist.

If you need:

  • random access,
  • multiple iterations,
  • sorting,
  • filtering with array functions,
  • or frequent lookups,

an array is often the simpler and more appropriate solution.

As with many performance-related features, the best choice depends on the problem you're solving—not on the feature itself.


13. Frequently Asked Questions

Are Generators faster than arrays?

Not necessarily.

A common misconception is that generators improve execution speed. Their primary advantage is reducing memory consumption, not making code run faster.

For small datasets, arrays are often just as fast—or even slightly faster—because all values are already available in memory.

Generators become advantageous when processing large datasets that would otherwise require significant memory allocation.


Do Generators save memory?

Yes.

Instead of storing every value in an array, a generator produces each value only when requested and keeps only its current execution state in memory.

For workloads involving thousands or millions of items, this can reduce memory usage dramatically.


What is the difference between yield and return?

The return statement immediately ends the function and returns a single value to the caller.

function greeting(): string
{
    return 'Hello';
}

The yield keyword, on the other hand, produces a value without terminating the function.

function letters(): Generator
{
    yield 'A';
    yield 'B';
    yield 'C';
}

After each yield, execution pauses and resumes when the next value is requested.


Can a Generator return a value?

Yes.

Although generators are primarily used to produce values with yield, they can also finish execution with a return statement.

function process(): Generator
{
    yield 'Step 1';
    yield 'Step 2';

    return 'Completed';
}

Unlike yielded values, the value returned by return is not produced during iteration.

$generator = process();

foreach ($generator as $step) {
    echo $step . PHP_EOL;
}

echo $generator->getReturn();

Output:

Step 1
Step 2
Completed

The returned value can be retrieved using the generator's getReturn() method after the generator has finished executing.

Attempting to access it before iteration is complete results in an exception:

$generator = process();

echo $generator->getReturn();
Exception: Cannot get return value of a generator that hasn't returned

In practice, this feature is rarely needed in everyday applications. It is most useful when a generator needs to report a final result after producing all of its values—for example, the total number of processed records, execution statistics, or a completion status.

function parseRecords(array $records): Generator
{
    $processed = 0;

    foreach ($records as $record) {
        yield strtoupper($record);
        $processed++;
    }

    return $processed;
}

$generator = parseRecords(['php', 'generator', 'yield']);

foreach ($generator as $record) {
    echo $record . PHP_EOL;
}

echo "Processed: {$generator->getReturn()} records";

Output:

PHP
GENERATOR
YIELD
Processed: 3 records

While this capability is part of the language, most generators simply use yield to produce values and terminate without returning an additional result.


What does yield from do?

yield from delegates iteration to another iterable.

Instead of manually looping over another generator and yielding each value yourself, PHP forwards every value automatically.

It results in cleaner, more readable code and is generally the preferred way to compose generators.


Can a Generator be rewound?

Generally, no.

Once a generator has advanced through its execution, it cannot simply be restarted.

If you need to iterate over the same sequence again, create a new generator instance by calling the generator function again.


Should I replace all arrays with Generators?

No.

Arrays and generators solve different problems.

Arrays are ideal for small collections, random access, sorting, and situations where the data must be reused multiple times.

Generators are most beneficial when processing large or streaming datasets sequentially.

Choosing one over the other should be based on the requirements of your application rather than on perceived performance gains.


Conclusion

Generators are one of PHP's most elegant features for writing memory-efficient, lazy-evaluated code. By introducing the yield keyword, they allow functions to produce values incrementally instead of allocating an entire collection upfront.

Throughout this article, you've learned how generators work internally, how yield differs from return, how to compose generators with yield from, and where they provide the greatest benefit in real-world applications. You've also seen that generators are especially well suited for processing large datasets, streaming data, reading files, and implementing lazy pipelines.

It's equally important to understand what generators are not. They are not a universal replacement for arrays, nor are they a magic performance optimization. Their primary strength is reducing memory usage while enabling sequential processing of data that doesn't need to exist entirely in memory at the same time.

As a rule of thumb:

  • Use arrays when you need random access, multiple iterations, sorting, or frequent manipulation of the collection.
  • Use generators when values can be produced and consumed one at a time.

Understanding this distinction will help you write PHP applications that are not only more efficient but also cleaner, more scalable, and easier to maintain. In many scenarios, simply replacing an eager array with a well-designed generator can turn an otherwise memory-intensive task into one that scales gracefully, regardless of the size of the data being processed.