Contents
- 1 Why iterators in PHP feel like stories we tell our code
- 2 Traversable: the invisible contract
- 3 Iterator: five methods that describe movement
- 4 IteratorAggregate: “I don’t walk, but I know who does”
- 5 Generators: the lazy iterator you didn’t have to write
- 6 Spl iterators: the strange toolbox we ignore
- 7 Recursive iterators: walking trees the calm way
- 8 Iterator interfaces and real PHP jobs
- 9 When should you reach for an iterator?
- 10 A small, realistic example: streaming report with filters
- 11 IteratorAggregate vs generators in everyday work
- 12 Iterators and the human side of PHP
- 13 Closing the loop
Why iterators in PHP feel like stories we tell our code
There’s a quiet moment many of us know well.
It’s late. The office is half-empty, or maybe you’re at home with headphones on. The ticket looks simple: “Refactor this data processing logic.” You open the file and see a mess of for loops, counters, array_map nesting inside foreach nesting inside “what was I thinking?”.
It works. But it’s brittle. Every new requirement means another condition inside another loop, until you end up scrolling and scrolling and wondering when code stopped being readable and turned into survival.
Somewhere between foreach ($items as $i => $item) and that nested if you don’t want to touch, there is a pattern trying to show itself:
- We walk over a collection.
- We decide how to walk (eager, lazy, filtered, batched).
- We decide what to do at each step.
That’s all an iterator is: a way to tell the story of “how we walk through data” in a first-class, explicit way.
PHP gives us interfaces for this. Most of us meet them via SPL, usually by accident, when reading someone else’s code. Iterator, IteratorAggregate, Traversable, Generator, then the whole family: SeekableIterator, RecursiveIterator, OuterIterator, FilterIterator, LimitIterator, CachingIterator…
They sound more like a council of wizards than something you’d use on a Tuesday morning in a legacy Laravel app.
But they’re not exotic. They’re practical. And if you’re working in PHP professionally—whether you’re looking for a job, hiring, or just trying to feel slightly less chaotic in your daily work—understanding iterator interfaces gives you a new mental tool.
Not a new framework. A new way of thinking.
Let’s walk through them. Slowly. Like we’re actually allowed to breathe while coding.
Traversable: the invisible contract
In PHP, Traversable is an internal interface. You can’t implement it yourself. You see it mostly in type hints:
function processItems(Traversable $items): void
{
foreach ($items as $item) {
// ...
}
}
This says: “Give me something I can foreach over. I don’t care if it’s an ArrayIterator, a Generator, or a custom class implementing Iterator.”
This is already a small but important shift.
Instead of “I need an array,” you say:
I need something I can walk through.
Arrays still work—PHP lets you foreach normal arrays—but typed code can embrace Traversable as the generic contract for iterable objects.
When you’re designing libraries or APIs, that shift matters. It makes your code more flexible and more honest. You’re telling the caller what you truly need: not an array, just something walkable.
Iterator: five methods that describe movement
Iterator is where most of us actually touch the iterator system.
To implement Iterator, you define:
interface Iterator extends Traversable
{
public function current(): mixed;
public function key(): mixed;
public function next(): void;
public function rewind(): void;
public function valid(): bool;
}
Five methods. That’s all.
current()– what is herekey()– where we arenext()– move forwardrewind()– go to the startvalid()– are we still inside the collection?
That’s basically the anatomy of any walk: where you start, where you are now, how to move, how to know when to stop.
Here is a tiny, concrete example: a lazy range iterator.
class RangeIterator implements Iterator
{
private int $start;
private int $end;
private int $step;
private ?int $current;
public function __construct(int $start, int $end, int $step = 1)
{
if ($step === 0) {
throw new InvalidArgumentException('Step must not be zero.');
}
$this->start = $start;
$this->end = $end;
$this->step = $step;
}
public function current(): int
{
return $this->current;
}
public function key(): int
{
return (int) floor(($this->current - $this->start) / $this->step);
}
public function next(): void
{
$this->current += $this->step;
}
public function rewind(): void
{
$this->current = $this->start;
}
public function valid(): bool
{
return $this->step > 0
? $this->current <= $this->end
: $this->current >= $this->end;
}
}
And then:
$range = new RangeIterator(1, 10, 2);
foreach ($range as $i => $value) {
echo "$i => $value\n";
}
It prints:
0 => 1
1 => 3
2 => 5
3 => 7
4 => 9
Simple. Readable. Lazy.
Instead of building an array with all values up front, you’ve turned the sequence into a story the interpreter can walk through step by step.
Now imagine this idea applied not to numbers, but to:
- rows in a huge CSV,
- messages from Kafka,
- paginated API responses,
- database cursors.
Not everything has to be a giant array loaded into memory. Sometimes we don’t want to hold everything at once. Sometimes it’s enough to just know what comes next.
IteratorAggregate: “I don’t walk, but I know who does”
Iterator is for classes that themselves know how to walk through data.
IteratorAggregate is similar, but a bit more indirect:
interface IteratorAggregate extends Traversable
{
public function getIterator(): Traversable;
}
It’s like saying:
I’m not the iterator. But I can give you one.
Use IteratorAggregate when your object is more of a collection or a rich domain object, and its job is not to manage the nitty-gritty of iteration, but to expose a clean way to get an iterator.
Example:
class UserCollection implements IteratorAggregate
{
/** @var User[] */
private array $users = [];
public function add(User $user): void
{
$this->users[] = $user;
}
public function getIterator(): Traversable
{
return new ArrayIterator($this->users);
}
}
Now:
$users = new UserCollection();
$users->add(new User('Alice'));
$users->add(new User('Bob'));
foreach ($users as $user) {
echo $user->getName(), PHP_EOL;
}
You don’t expose raw arrays. You expose behavior. But you still stay friendly to foreach.
There’s something quietly satisfying about this. Your domain object says “you can walk through me like an array,” but under the hood it can change implementation later without breaking callers.
It’s a small kind of future-proofing, and those add up over the years.
Generators: the lazy iterator you didn’t have to write
The older I get as a developer, the more I appreciate laziness.
- Laziness in execution: don’t compute what you don’t need.
- Laziness in development: don’t build more infrastructure than needed.
PHP’s generators give you both. They implement Iterator for you, behind the scenes. You just write a function and use yield.
function readLines(string $path): Traversable
{
$handle = fopen($path, 'rb');
if (!$handle) {
throw new RuntimeException("Cannot open file: $path");
}
try {
while (($line = fgets($handle)) !== false) {
yield rtrim($line, "\r\n");
}
} finally {
fclose($handle);
}
}
Usage:
foreach (readLines('/var/log/app.log') as $line) {
if (str_contains($line, 'ERROR')) {
// ...
}
}
You never store the whole file in memory. You read line by line. The iterator interface is honored, but you don’t implement it yourself.
Under the hood, that generator object is an Iterator. current(), key(), next(), valid(), rewind() — all there, all hidden.
This is probably the most “real-world” way PHP developers use iterators today, especially in frameworks and data-heavy code:
- streaming export of reports,
- big log files,
- chunked queues,
- lazy evaluation pipelines.
If you’re preparing for a PHP interview, or you’re hiring and want to gauge depth, a simple question reveals a lot:
When would you use
yieldinstead of building an array?
The answer usually tells you whether the person has ever bumped into memory limits at 3 AM and had to rethink their approach.
Spl iterators: the strange toolbox we ignore
SPL iterators in PHP are like that shelf in the office kitchen where someone keeps a random collection of adapters and utensils: weird at first glance, surprisingly handy when you know what’s there.
You have:
ArrayIteratorDirectoryIteratorRecursiveDirectoryIteratorFilterIteratorCallbackFilterIteratorLimitIteratorCachingIteratorRecursiveIteratorIteratorRegexIteratorAppendIterator- and more.
They look intimidating in the manual, but most follow a simple pattern: they either wrap another iterator (decorator pattern) or provide a specific way to traverse something.
Filtering without losing your mind
Let’s say you want to iterate only over even numbers in your custom RangeIterator.
class EvenFilterIterator extends FilterIterator
{
public function accept(): bool
{
return $this->current() % 2 === 0;
}
}
$range = new RangeIterator(1, 20);
$evenRange = new EvenFilterIterator($range);
foreach ($evenRange as $value) {
echo $value, ' ';
}
// 2 4 6 8 10 12 14 16 18 20
No extra if inside foreach. The filtering logic lives in a reusable, testable place.
If you don’t want to write a class, CallbackFilterIterator helps:
$range = new RangeIterator(1, 20);
$evenRange = new CallbackFilterIterator(
$range,
fn (int $current, int $key, Iterator $iterator): bool => $current % 2 === 0
);
This is where iterators become composition-friendly:
- one iterator defines the raw sequence,
- another filters,
- another limits,
- another caches, etc.
You start to move away from huge, imperative loops and toward smaller, stackable units.
Limiting and batching
LimitIterator lets you take just a slice:
$iterator = new ArrayIterator(range(1, 100));
$firstTenItems = new LimitIterator($iterator, 0, 10);
This feels like array_slice, but lazy. You don’t have to materialize everything up front.
Combine with generators, and you can build simple, readable pipelines for streaming processing. No frameworks. Just native tools.
Recursive iterators: walking trees the calm way
Trees are everywhere:
- nested categories,
- filesystem directories,
- comment threads,
- menu structures,
- JSON documents.
You can manually write recursive functions to walk them. Many of us do. Then we rediscover RecursiveIterator and RecursiveIteratorIterator.
A concrete example: recursive directory listing
$directory = new RecursiveDirectoryIterator(
'/var/www',
FilesystemIterator::SKIP_DOTS
);
$iterator = new RecursiveIteratorIterator(
$directory,
RecursiveIteratorIterator::SELF_FIRST
);
foreach ($iterator as $path => $info) {
echo $path, PHP_EOL;
}
You get a depth-first clone of find /var/www, in PHP, using three lines of setup and a foreach.
If you need to filter only .php files:
class PhpFileFilter extends FilterIterator
{
public function accept(): bool
{
/** @var SplFileInfo $file */
$file = $this->current();
return $file->isFile()
&& $file->getExtension() === 'php';
}
}
$recursiveIterator = new RecursiveIteratorIterator($directory);
$phpFiles = new PhpFileFilter($recursiveIterator);
foreach ($phpFiles as $file) {
echo $file->getRealPath(), PHP_EOL;
}
A few lines, and you have a lazily traversed, filtered tree.
I remember the first time I replaced a tangled recursive function with a RecursiveIteratorIterator pipeline. There was this small, strange feeling of relief, like finding a built-in shortcut you’d been re-implementing by hand for years.
It doesn’t fix the deadline. But it does make the code feel lighter.
Iterator interfaces and real PHP jobs
It can feel like iterators live in a rarefied air: textbooks, interview questions, obscure SPL docs. But on a platform like Find PHP, where people are actually looking for work or talent, iterators quietly show up as a differentiator.
For employers:
- A candidate who understands
Traversable,IteratorAggregate, and generators usually writes more memory-conscious code. - They’re more likely to model complex data flows with small, composable pieces instead of one monster function.
- They can work with large datasets without reaching for premature caching or crazy workarounds.
For developers looking for a job:
- Demonstrating real-world use of iterators in your GitHub projects or portfolio says, “I’ve met scaling problems and I’ve learned from them.”
- Knowing how to combine SPL iterators can impress not by buzzwords, but by elegance.
In a market where everyone claims they “know PHP,” small signs of depth matter.
When should you reach for an iterator?
You don’t need iterators everywhere. Vanilla arrays and foreach are still the bread and butter of PHP.
But certain situations are natural fits:
-
Large datasets
Reading from files, streams, APIs, or DB cursors where loading everything into memory is expensive. -
Pipelines
When data goes through multiple transformations: map → filter → enrich → output.
Iterators let each step be its own object (or generator). -
Collections in your domain model
Collections that want to expose nice iteration semantics without becoming just arrays. -
Recursive structures
Trees, directories, nested entities where a depth-first or breadth-first traversal is needed. -
Clean separation of concerns
When you want iteration logic separate from business logic, iterators give you a natural boundary.
There’s also a softer, almost emotional reason: code written with iterator interfaces often reads like a series of clear, deliberate moves instead of one long blur of mutations.
You can skim it six months later and still understand the flow without your brain overheating.
A small, realistic example: streaming report with filters
Let’s put some pieces together in a way that feels like “Tuesday at work”.
Imagine:
- You have a large table of orders.
- You need to generate a CSV of recent orders above some value.
- The table is big enough that you don’t want to load it all into memory.
We’ll sketch it out.
class OrderStream implements Iterator
{
private PDOStatement $stmt;
private ?array $current = null;
private int $position = 0;
public function __construct(PDO $pdo)
{
$this->stmt = $pdo->query(
'SELECT id, user_id, total, created_at FROM orders ORDER BY created_at DESC'
);
}
public function current(): array
{
return $this->current;
}
public function key(): int
{
return $this->position;
}
public function next(): void
{
$this->current = $this->stmt->fetch(PDO::FETCH_ASSOC) ?: null;
$this->position++;
}
public function rewind(): void
{
// Not truly rewindable without re-querying.
// For simplicity we just throw if someone tries.
throw new LogicException('OrderStream is forward-only.');
}
public function valid(): bool
{
return $this->current !== null;
}
}
Then a filter:
class MinimumTotalFilter extends FilterIterator
{
private float $minTotal;
public function __construct(Iterator $iterator, float $minTotal)
{
parent::__construct($iterator);
$this->minTotal = $minTotal;
}
public function accept(): bool
{
$order = $this->current();
return (float) $order['total'] >= $this->minTotal;
}
}
And a generator for CSV lines:
function ordersToCsvLines(Traversable $orders): Traversable
{
// Header
yield ['id', 'user_id', 'total', 'created_at'];
foreach ($orders as $order) {
yield [
$order['id'],
$order['user_id'],
$order['total'],
$order['created_at'],
];
}
}
Streaming the file to the browser:
$pdo = new PDO(/* ... */);
$stream = new OrderStream($pdo);
$filtered = new MinimumTotalFilter($stream, 100.00);
$csvIterator = ordersToCsvLines($filtered);
$fh = fopen('php://output', 'wb');
foreach ($csvIterator as $row) {
fputcsv($fh, $row);
}
fclose($fh);
This is not “toy” code. This is the kind of pattern you can drop into a Symfony or Laravel app. If your interviewer asks you how you’d handle large exports without killing memory, this is a solid answer. If you’re the interviewer, and someone describes something like this unprompted, you probably lean forward a bit.
There’s a feel to code like this. It's calm. It doesn’t panic. It doesn’t try to hold everything at once.
IteratorAggregate vs generators in everyday work
There’s often a quiet question hiding in the back of our minds:
Should I implement
Iterator, useIteratorAggregate, or just write a generator?
Some rough heuristics from real projects:
-
Use generators (
yield) when:- You have straightforward sequential logic.
- You don’t need random access or rewinding.
- You care about streaming and simplicity.
-
Use IteratorAggregate when:
- You already have a data structure (like a collection object).
- You want to internally use arrays, iterators, or generators.
- You want to keep your public API simple:
foreach ($collection as $item).
-
Implement Iterator directly when:
- You need full control.
- You’re building a reusable, low-level class.
- You want to integrate deeply with SPL iterators (filters, limits, recursion).
In other words, reach for the lightest tool that does the job. Most days, that’s a generator or IteratorAggregate returning an ArrayIterator or generator.
Iterators and the human side of PHP
On a platform like Find PHP, it’s easy to focus on the visible stuff:
- frameworks listed on a CV,
- “years of experience”,
- job titles,
- buzzwords like “microservices” or “DDD” or “event sourcing”.
But in day-to-day work, what separates a solid PHP developer from an overwhelmed one is often their ability to shape complexity.
Iterator interfaces are one example of that shaping. They’re not glamorous. You won’t see them on product landing pages. But they’re part of that deeper layer of understanding that lets you:
- keep big data flows readable,
- avoid subtle memory issues,
- design APIs that age well,
- refactor legacy without making everything worse.
If you’re hiring, notice candidates who understand things like this and can explain them calmly. They tend to be the ones who will keep your codebase from turning into an unmaintainable storm over the next few years.
If you’re looking for a job, remember that skills like this are not noise: they are the quiet signal that you care about how code behaves under real load, over real time, in real teams.
Closing the loop
At some point, most of us have stared at a function so long it felt like static in the brain. Nesting inside nesting. Database calls hidden inside loops hidden inside conditionals. You fix the bug, but you leave with a faint sense that something is… off.
Learning PHP’s iterator interfaces doesn’t magically fix everything. But it gives you a new way to untangle those messes.
You start thinking:
- “This is actually a sequence.”
- “I can separate what I walk from how I transform it.”
- “I don’t need everything in memory at once.”
- “This part could be a generator, that part a filter iterator, and suddenly I can breathe again.”
And on some late evening, when the office is quiet and your code runs smoothly over gigabytes of data without breaking a sweat, you feel that small, private satisfaction.
Not because you used a clever feature.
Because you walked through complexity one step at a time, and somehow, this time, you didn’t get lost.