Skip to content
Iterators and ArrayAccess
step 1/6

Reading — step 1 of 6

Learn

~2 min readIteration Protocols

Implement these interfaces and your custom objects gain foreach / [] syntax — the same as built-in arrays.

Iterator interface

Five methods. Implement them and foreach knows how to walk your object:

class Range implements \Iterator {
    private int $pos;
    
    public function __construct(
        private int $start,
        private int $end,
    ) { $this->pos = $start; }
    
    public function current(): mixed   { return $this->pos; }
    public function key(): mixed       { return $this->pos - $this->start; }
    public function next(): void       { $this->pos++; }
    public function rewind(): void     { $this->pos = $this->start; }
    public function valid(): bool      { return $this->pos < $this->end; }
}

foreach (new Range(5, 10) as $key => $value) {
    echo "$key => $value\n";
}
// 0 => 5
// 1 => 6 ... 4 => 9

The protocol:

  1. rewind() — reset to the beginning
  2. valid() — is there a current element?
  3. current() / key() — the element and its key
  4. next() — advance
  5. back to step 2

In practice, use \Generator (the previous lesson) — it gives you an Iterator with one yielded value per iteration.

IteratorAggregate — the easy way

For most cases, implement the simpler IteratorAggregate:

class UserCollection implements \IteratorAggregate {
    public function __construct(private array $users) {}
    public function getIterator(): \Generator {
        foreach ($this->users as $u) yield $u;
    }
}

Return anything that's already iterable — including a generator. Much less code than the 5-method Iterator protocol.

ArrayAccess — bracket syntax

Make $obj[$key] work:

class Config implements \ArrayAccess {
    public function __construct(private array $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 $value): void {
        if ($key === null) $this->data[] = $value;
        else $this->data[$key] = $value;
    }
    public function offsetUnset(mixed $key): void {
        unset($this->data[$key]);
    }
}

$c = new Config(['theme' => 'dark']);
echo $c['theme'];        // dark — offsetGet
$c['lang'] = 'en';        // offsetSet
isset($c['theme']);       // offsetExists
unset($c['lang']);        // offsetUnset

Countable — count() support

class Cart implements \Countable {
    public function __construct(private array $items = []) {}
    public function count(): int { return count($this->items); }
}

echo count(new Cart(['a', 'b', 'c']));   // 3

Combining them

A real collection class often implements all three: IteratorAggregate, ArrayAccess, Countable. Then it walks like an array, indexes like an array, counts like an array.

Common mistakes

  • Implementing Iterator from scratch when IteratorAggregate + a generator would be simpler.
  • Forgetting rewind() must reset state — foreach rewinds at the start. If state isn't reset, the second foreach sees nothing.
  • ArrayAccess offsetSet receiving null key — that's $obj[] = $value — append. Handle it.
  • Mutating during iteration — same gotcha as arrays. Snapshot if you need to mutate.

Discussion

Ask a question, share an insight, or help someone who’s stuck.

Sign in to post a comment or reply.

Loading…