Iterators
What Is an Iterator?
In simple terms: An iterator is an object that knows how to walk through a sequence of items one by one. When you write foreach ($something as $item), PHP asks an iterator for the first item, then the next, and so on, until there are no more. By writing your own iterator, you decide what a foreach loop over your object produces — the days in a date range, the rows of a report, or the pages of an API.
Technically: PHP's foreach works with arrays and with objects implementing the Traversable interface. User classes become traversable by implementing either:
| Interface | You Implement | Best For |
|---|---|---|
Iterator | Five methods: current(), key(), next(), rewind(), valid() | Full control over iteration state |
IteratorAggregate | One method: getIterator(), returning any Traversable | Most cases — simple and concise |
PHP's Standard PHP Library (SPL) also provides ready-made iterators such as ArrayIterator, LimitIterator, and CallbackFilterIterator.
Default Object Iteration
Even without implementing anything, foreach can loop over an object. It then visits the object's properties that are visible from the current scope:
<?php
class Product
{
public string $name = 'Gel Pen';
public int $price = 20;
protected int $cost = 12;
private string $supplier = 'Classmate';
public function listAllFields(): void
{
foreach ($this as $field => $value) {
echo " $field = $value\n";
}
}
}
$product = new Product();
echo "From outside:\n";
foreach ($product as $field => $value) {
echo " $field = $value\n";
}
echo "From inside:\n";
$product->listAllFields();
Output
From outside:
name = Gel Pen
price = 20
From inside:
name = Gel Pen
price = 20
cost = 12
supplier = Classmate
Default iteration exposes the object's structure, which is rarely what a user of the class should depend on. Implementing an iterator interface lets the class decide what iteration means.
The Iterator Interface
The Five Methods
| Method | Purpose |
|---|---|
rewind(): void | Move to the first element (called once at the start of foreach) |
valid(): bool | Is there a current element? (false ends the loop) |
current(): mixed | Return the current element |
key(): mixed | Return the key of the current element |
next(): void | Move to the next element |
The Order in Which foreach Calls Them
<?php
final class TracingIterator implements Iterator
{
private int $position = 0;
public function __construct(private array $items) {}
public function rewind(): void { echo "rewind\n"; $this->position = 0; }
public function valid(): bool { echo "valid\n"; return isset($this->items[$this->position]); }
public function current(): mixed { echo "current\n"; return $this->items[$this->position]; }
public function key(): mixed { echo "key\n"; return $this->position; }
public function next(): void { echo "next\n"; $this->position++; }
}
foreach (new TracingIterator(['a', 'b']) as $key => $value) {
echo " body: $key => $value\n";
}
Output
rewind
valid
current
key
body: 0 => a
next
valid
current
key
body: 1 => b
next
valid
PHP calls rewind() once, then repeats valid → current → key → body → next until valid() returns false. (key() is only called when the loop asks for a key.)
Practical Example: Iterating Over a Date Range
<?php
final class DateRange implements Iterator
{
private DateTimeImmutable $current;
private int $index = 0;
public function __construct(
private DateTimeImmutable $start,
private DateTimeImmutable $end,
) {
$this->current = $start;
}
public function rewind(): void
{
$this->current = $this->start;
$this->index = 0;
}
public function valid(): bool
{
return $this->current <= $this->end;
}
public function current(): DateTimeImmutable
{
return $this->current;
}
public function key(): int
{
return $this->index;
}
public function next(): void
{
$this->current = $this->current->modify('+1 day');
$this->index++;
}
}
$batchDays = new DateRange(new DateTimeImmutable('2026-10-15'), new DateTimeImmutable('2026-10-18'));
foreach ($batchDays as $day => $date) {
echo 'Day ' . ($day + 1) . ': ' . $date->format('D, d M') . "\n";
}
echo iterator_count($batchDays) . " days in total\n";
Output
Day 1: Thu, 15 Oct
Day 2: Fri, 16 Oct
Day 3: Sat, 17 Oct
Day 4: Sun, 18 Oct
4 days in total
How It Works
- The iterator keeps its position (
$currentand$index) as object state. valid()ends the loop after the end date;next()advances one day using an immutable date.- Because
rewind()resets the state, the same range can be iterated again — here byiterator_count(). (Generators, by contrast, cannot be rewound.) - The range never builds an array of dates; each date is calculated when needed.
IteratorAggregate: The Simpler Choice
Most collection classes do not need to manage iteration state themselves. IteratorAggregate requires only one method that returns an iterator — often a generator:
<?php
final class Playlist implements IteratorAggregate, Countable
{
private array $lessons = [];
public function add(string $title, int $minutes): void
{
$this->lessons[] = ['title' => $title, 'minutes' => $minutes];
}
public function getIterator(): Generator
{
foreach ($this->lessons as $number => $lesson) {
yield $number + 1 => $lesson;
}
}
public function count(): int
{
return count($this->lessons);
}
}
$playlist = new Playlist();
$playlist->add('PHP Variables', 12);
$playlist->add('PHP Arrays', 18);
$playlist->add('PHP Functions', 15);
foreach ($playlist as $number => $lesson) {
echo "$number. {$lesson['title']} ({$lesson['minutes']} min)\n";
}
echo count($playlist) . " lessons\n";
Output
1. PHP Variables (12 min)
2. PHP Arrays (18 min)
3. PHP Functions (15 min)
3 lessons
Each foreach calls getIterator() again and receives a new generator, so the playlist can be looped any number of times. The internal array stays private; callers see only what the generator yields (here, with 1-based lesson numbers as keys).
SPL Iterators
PHP includes reusable iterators that can be combined:
<?php
$products = new ArrayIterator([
['name' => 'Gel Pen', 'stock' => 120],
['name' => 'Desk Lamp', 'stock' => 0],
['name' => 'Notebook', 'stock' => 45],
['name' => 'Backpack', 'stock' => 8],
]);
$inStock = new CallbackFilterIterator($products, fn(array $p): bool => $p['stock'] > 0);
$firstTwo = new LimitIterator($inStock, 0, 2);
foreach ($firstTwo as $product) {
echo $product['name'] . "\n";
}
Output
Gel Pen
Notebook
| Iterator | Purpose |
|---|---|
ArrayIterator | Wraps an array in an iterator object |
CallbackFilterIterator | Keeps only elements for which a callback returns true |
LimitIterator | Returns a slice (offset and count) |
InfiniteIterator, AppendIterator, MultipleIterator | Repeat, chain, or combine iterators |
DirectoryIterator, RecursiveDirectoryIterator | Walk through directories (see PHP Directories) |
Common Mistakes
Exposing Internals Through Default Iteration
Effect: Callers depend on public property names.
Fix: Implement IteratorAggregate to define what iteration returns.
Forgetting to Reset State in rewind()
Effect: The second loop over the object produces nothing.
Fix: Reset all position-related state in rewind().
Implementing Iterator When IteratorAggregate Suffices
Effect: Five methods of boilerplate and more room for bugs.
Fix: Return a generator or ArrayIterator from getIterator().
Wrong Return Types
Effect: Deprecation notices or errors in PHP 8.1+ when interface methods lack compatible return types.
Fix: Declare mixed, void, bool, or a more specific type as required by the interface.
Best Practices
- Use
IteratorAggregatewith a generator for most collection classes. - Implement
Iteratorwhen iteration needs explicit, resettable state (ranges, cursors). - Keep internal arrays private and expose only iteration.
- Combine SPL iterators for filtering and limiting without creating intermediate arrays.
- Implement
Countabletoo when the number of elements is known.
Official reference: https://www.php.net/manual/en/language.oop5.iterations.php