0

Object iteration — implementing Traversable, Iterator, IteratorAggregate

Intermediate5 min read·php-07-018
compare

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
<?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';