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 implementscurrent(),key(),next(),rewind()andvalid(); -
IteratorAggregate: the object provides an iterator fromgetIterator()— usually a generator or anArrayIterator, 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 |
|---|---|
|
iterates an array (or object properties); supports sorting, counting, offset access |
|
entries of a directory as |
|
a whole directory tree |
|
files matching a glob pattern |
|
lines of a file, optionally parsed as CSV |
|
keeps elements (or keys) matching a regular expression, or transforms them |
|
keeps elements for which a callback returns |
|
an offset/count window, for pagination |
|
knows whether there is a next element ( |
|
restarts at the end; combine with |
|
concatenate several iterators, or iterate several in parallel |
|
adapt |
<?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.
Source: SPL Iterators and iterator_to_array
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)
Source: RecursiveDirectoryIterator and RecursiveIteratorIterator
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.
Source: Generators overview and Generator syntax
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.
Source: Generator::send and The Generator class
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 acceptedTraversablebefore 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; passescape: ''.