Object iteration — implementing Traversable, Iterator, IteratorAggregate
Concept
PHP's foreach statement works on any object implementing Traversable. There are two interfaces in the hierarchy: Iterator (implement iteration yourself) and IteratorAggregate (delegate to another traversable).
Iterator requires five methods: current(), key(), next(), rewind(), valid(). These map directly to the five operations foreach performs: rewind() at loop start, then valid()/current()/key()/next() per iteration. Implementing Iterator gives full control over iteration behavior — lazy loading, stateful iteration, custom key generation.
IteratorAggregate requires one method: getIterator() which returns a Traversable. Simpler than Iterator — just delegate to an existing iterator (an array wrapped in ArrayIterator, a Generator, or another custom iterator). Use this when your class holds a collection and iteration should just be "iterate the collection."
ArrayAccess: A separate but related interface for array-like access ($obj['key']). Requires offsetExists, offsetGet, offsetSet, offsetUnset. Lets objects be used with isset($obj['key']) and $obj['key'] = value syntax.
Countable: Enables count($obj). One method: count(). Often implemented alongside IteratorAggregate to create collection classes.
SPL iterators: PHP provides ArrayIterator, DirectoryIterator, FilterIterator, LimitIterator, and more — ready-made iterator implementations for common needs.
Code Example
<?php
declare(strict_types=1);
// Custom Iterator — lazy number range
class NumberRange implements Iterator
{
private int $current;
public function __construct(
private int $start,
private int $end,
private int $step = 1,
) {
$this->current = $start;
}
public function current(): int { return $this->current; }
public function key(): int { return ($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->current <= $this->end; }
}
foreach (new NumberRange(1, 10, 2) as $i => $n) {
echo "$i: $n\n"; // 0: 1, 1: 3, 2: 5, 3: 7, 4: 9
}
// IteratorAggregate — simpler delegation pattern
class UserCollection implements IteratorAggregate, Countable
{
private array $users = [];
public function add(array $user): void { $this->users[] = $user; }
public function getIterator(): ArrayIterator { return new ArrayIterator($this->users); }
public function count(): int { return count($this->users); }
}
$coll = new UserCollection();
$coll->add(['name' => 'Alice']);
$coll->add(['name' => 'Bob']);
echo count($coll); // 2
foreach ($coll as $user) { echo $user['name'] . "\n"; }
// IteratorAggregate with Generator — lazy loading
class PaginatedUsers implements IteratorAggregate
{
public function __construct(private int $total) {}
public function getIterator(): Generator
{
for ($page = 1; $page <= ceil($this->total / 50); $page++) {
$users = fetchUsersPage($page, 50); // lazy: one page at a time
yield from $users;
}
}
}
// ArrayAccess — object-as-array syntax
class Config implements ArrayAccess
{
private array $data;
public function __construct(array $data) { $this->data = $data; }
public function offsetExists(mixed $key): bool { return isset($this->data[$key]); }
public function offsetGet(mixed $key): mixed { return $this->data[$key] ?? null; }
public function offsetSet(mixed $key, mixed $val): void { $this->data[$key] = $val; }
public function offsetUnset(mixed $key): void { unset($this->data[$key]); }
}
$cfg = new Config(['debug' => true, 'db' => 'mysql']);
echo $cfg['debug']; // true — via ArrayAccess
$cfg['cache'] = 'redis';