Iterators and Generators

This content was generated with the assistance of AI and should be verified against the official documentation before being relied on in production.

The reference material consulted while preparing these pages is listed in the section bibliography.

This page is written against PHP 8.5, checked on 2026-10-05. Anything foreach can loop over is either an array or an object implementing Traversable. This page covers the iterator interfaces, the ready-made SPL iterators, and generators, the simplest way to write iterators — and the key to processing data sets larger than memory.

Traversable, Iterator and IteratorAggregate

Traversable is a marker interface: classes cannot implement it directly, but through one of its two sub-interfaces:

  • Iterator: the object is the iterator and implements current(), key(), next(), rewind() and valid();

  • IteratorAggregate: the object provides an iterator from getIterator() — usually a generator or an ArrayIterator, which is far less code.

<?php

declare(strict_types=1);

/** Iterates pages of a paginated result: an Iterator written by hand. */
final class PageIterator implements Iterator
{
    private int $page = 1;

    public function __construct(private readonly int $totalItems, private readonly int $perPage) {}

    public function current(): string { return sprintf('page %d of %d', $this->page, $this->pages()); }
    public function key(): int { return $this->page; }
    public function next(): void { $this->page++; }
    public function rewind(): void { $this->page = 1; }
    public function valid(): bool { return $this->page <= $this->pages(); }
    private function pages(): int { return (int) ceil($this->totalItems / $this->perPage); }
}

/** A collection: IteratorAggregate with a generator, much shorter. */
final class Shelf implements IteratorAggregate, Countable
{
    public function __construct(private array $titles) {}

    public function getIterator(): Generator
    {
        foreach ($this->titles as $position => $title) {
            yield $position + 1 => $title;
        }
    }

    public function count(): int { return count($this->titles); }
}

foreach (new PageIterator(totalItems: 45, perPage: 20) as $n => $label) {
    echo "{$n}: {$label}\n";
}
foreach (new Shelf(['Solaris', 'Kindred']) as $position => $title) {
    echo "#{$position} {$title}\n";
}
1: page 1 of 3
2: page 2 of 3
3: page 3 of 3
#1 Solaris
#2 Kindred

foreach calls rewind(), then valid(), current(), key(), the loop body and next() in a loop. Keys can be any type with iterators (arrays only allow int and string).

The SPL iterators

The SPL ships iterators that wrap other iterators, so they can be combined like Unix pipes:

Iterator Purpose

ArrayIterator

iterates an array (or object properties); supports sorting, counting, offset access

DirectoryIterator, FilesystemIterator

entries of a directory as SplFileInfo objects

RecursiveDirectoryIterator + RecursiveIteratorIterator

a whole directory tree

GlobIterator

files matching a glob pattern

SplFileObject

lines of a file, optionally parsed as CSV

RegexIterator

keeps elements (or keys) matching a regular expression, or transforms them

CallbackFilterIterator

keeps elements for which a callback returns true

LimitIterator

an offset/count window, for pagination

CachingIterator

knows whether there is a next element (hasNext()), handy for separators

InfiniteIterator

restarts at the end; combine with LimitIterator

AppendIterator, MultipleIterator

concatenate several iterators, or iterate several in parallel

NoRewindIterator, IteratorIterator

adapt Traversable objects, prevent rewinding

<?php

declare(strict_types=1);

$titles = new ArrayIterator(['Solaris', 'Kindred', 'Dawn', 'Imago', 'Wild Seed', 'Ariel']);

// Titles with a space or starting with a vowel, first page of 2
$filtered = new CallbackFilterIterator($titles, fn (string $t): bool => str_contains($t, ' ') || preg_match('/^[AEIOU]/', $t) === 1);
$page = new LimitIterator($filtered, offset: 0, limit: 2);
echo implode(', ', iterator_to_array($page, preserve_keys: false)), "\n";      // Imago, Wild Seed

// RegexIterator on the values
echo implode(', ', iterator_to_array(new RegexIterator($titles, '/^[DK]/'), false)), "\n";   // Kindred, Dawn

// CachingIterator: know when the last element comes
$it = new CachingIterator(new ArrayIterator(['Solaris', 'Kindred', 'Dawn']));
foreach ($it as $title) {
    echo $title, $it->hasNext() ? ' -> ' : "\n";       // Solaris -> Kindred -> Dawn
}

// Round-robin assignment of returns to three shelves
$shelves = new LimitIterator(new InfiniteIterator(new ArrayIterator(['A', 'B', 'C'])), 0, 5);
echo implode(' ', iterator_to_array($shelves, false)), "\n";                   // A B C A B

var_dump(iterator_count(new ArrayIterator([1, 2, 3])));                        // int(3)

iterator_to_array() collects an iterator into an array (pass false to discard keys, which avoids overwriting duplicates); iterator_count() counts by iterating; since PHP 8.2 both accept any iterable, including arrays.

Walking a directory tree

<?php

declare(strict_types=1);

$root = sys_get_temp_dir() . '/library-files';
@mkdir("{$root}/covers/sf", 0777, true);
file_put_contents("{$root}/covers/sf/solaris.jpg", 'x');
file_put_contents("{$root}/covers/kindred.png", 'xx');
file_put_contents("{$root}/notes.txt", 'xxx');

$files = new RecursiveIteratorIterator(
    new RecursiveDirectoryIterator($root, FilesystemIterator::SKIP_DOTS),
);
$images = new RegexIterator($files, '/\.(jpe?g|png)$/i');

$found = [];
foreach ($images as $path => $info) {                 // $info is an SplFileInfo
    $found[] = substr($path, strlen($root) + 1) . " ({$info->getSize()} bytes)";
}
sort($found);
echo implode("\n", $found), "\n";
// covers/kindred.png (2 bytes)
// covers/sf/solaris.jpg (1 bytes)

Generators

A function containing yield is a generator function: calling it does not run its body but returns a Generator object, an iterator whose values are produced on demand. Execution runs until the next yield, pauses there, and resumes when the consumer asks for the next value. Only one value exists at a time, so a generator can describe millions of items — or an infinite sequence — in constant memory.

<?php

declare(strict_types=1);

function loanIds(int $start): Generator
{
    echo "[start]\n";
    for ($id = $start; ; $id++) {          // infinite: the consumer decides when to stop
        yield $id;
    }
}

$ids = loanIds(1000);
echo "generator created, nothing ran yet\n";
foreach ($ids as $id) {
    echo $id, "\n";
    if ($id === 1002) {
        break;
    }
}
generator created, nothing ran yet
[start]
1000
1001
1002

A generator can only be iterated once and only forwards; calling rewind() after it has advanced throws an exception.

Keys, yield by reference and yield from

yield $key => $value yields a key/value pair (keys default to 0, 1, 2, …). function &gen() with yield $var yields references, so a foreach (… as &$v) loop can modify the generator’s variables. yield from delegates to another generator, array or Traversable, re-yielding everything it produces; its value is the inner generator’s return value.

<?php

declare(strict_types=1);

function sfBooks(): Generator
{
    yield '978-0-00-000003-5' => 'Solaris';
    yield '978-0-00-000001-1' => 'Kindred';
    return 2;                                   // a generator can return a value
}

function poetry(): Generator
{
    yield '978-0-00-000009-7' => 'Ariel';
    return 1;
}

function catalogue(): Generator
{
    $sf = yield from sfBooks();                 // delegation: re-yields keys and values
    $poems = yield from poetry();
    return $sf + $poems;
}

$all = catalogue();
foreach ($all as $isbn => $title) {
    echo "{$isbn}: {$title}\n";
}
echo 'total: ', $all->getReturn(), "\n";        // total: 3 -- available once the generator has finished

yield from keeps the inner generator’s keys, so two delegated generators that both start at key 0 produce duplicate keys; pass false to iterator_to_array() when collecting them.

Sending values into a generator

yield is also an expression: $x = yield $value; receives whatever the consumer passes to $generator->send(). This turns generators into coroutines that can consume a stream of values while keeping state. throw() raises an exception at the paused yield.

<?php

declare(strict_types=1);

/** Receives return dates and reports running statistics. */
function lateReturnTracker(): Generator
{
    $count = 0;
    $lateDays = 0;
    while (true) {
        $days = yield sprintf('%d returns, %d late days', $count, $lateDays);
        if ($days === null) {
            return $lateDays;
        }
        $count++;
        $lateDays += max(0, $days);
    }
}

$tracker = lateReturnTracker();
echo $tracker->current(), "\n";        // 0 returns, 0 late days  (runs to the first yield)
echo $tracker->send(3), "\n";          // 1 returns, 3 late days
echo $tracker->send(-1), "\n";         // 2 returns, 3 late days
echo $tracker->send(5), "\n";          // 3 returns, 8 late days
$tracker->send(null);
echo $tracker->getReturn(), "\n";      // 8

For concurrency, Fibers (8.1) replaced generator-based coroutines; see Fibers and Asynchronous PHP.

Lazy pipelines over large files

Generators shine when processing data that does not fit in memory: each stage pulls one row at a time from the previous stage. A pipeline that reads a CSV export of loans, filters overdue ones and aggregates fines never holds more than one row:

<?php

declare(strict_types=1);

// Create a sample export (in real life: hundreds of megabytes).
$file = sys_get_temp_dir() . '/loans.csv';
$out = new SplFileObject($file, 'w');
$out->fputcsv(['isbn', 'member', 'days_late'], escape: '');
foreach ([['978-0-00-000001-1', 'M-1', 3], ['978-0-00-000003-5', 'M-2', 0], ['978-0-00-000002-8', 'M-1', 12]] as $row) {
    $out->fputcsv($row, escape: '');
}
$out = null;                                         // close the file

/** @return Generator<int, array<string, string>> */
function readCsv(string $path): Generator
{
    $f = new SplFileObject($path);
    $f->setFlags(SplFileObject::READ_CSV | SplFileObject::SKIP_EMPTY | SplFileObject::READ_AHEAD);
    $f->setCsvControl(escape: '');
    $header = null;
    foreach ($f as $row) {
        if ($header === null) {
            $header = $row;
            continue;
        }
        yield array_combine($header, $row);
    }
}

function overdue(iterable $loans): Generator
{
    foreach ($loans as $loan) {
        if ((int) $loan['days_late'] > 0) {
            yield $loan;
        }
    }
}

function finesByMember(iterable $loans, int $centsPerDay): array
{
    $fines = [];
    foreach ($loans as $loan) {
        $fines[$loan['member']] = ($fines[$loan['member']] ?? 0) + (int) $loan['days_late'] * $centsPerDay;
    }
    return $fines;
}

print_r(finesByMember(overdue(readCsv($file)), 25));   // ["M-1" => 375]
echo 'peak memory stays flat: ', memory_get_peak_usage() < 4_000_000 ? 'yes' : 'no', "\n";

The empty escape argument follows PHP 8.4’s recommendation for CSV functions; see Files, Streams and I/O.

Where older books and tutorials are outdated

  • Generator return values and yield from (7.0) are missing from some PHP 5.5/5.6-era material.

  • iterator_to_array()/iterator_count() only accepted Traversable before 8.2; they now accept arrays too.

  • Generator-based coroutines for async I/O (as in early AMPHP and ReactPHP) have been replaced by Fibers (8.1).

  • each()-based manual iteration was removed in 8.0.

  • CSV escaping: relying on the default "\\" escape character is deprecated since 8.4; pass escape: ''.