skip to content

SPL Structures & Iterators

Iterator and IteratorAggregate make objects foreach-able, and SplQueue, SplPriorityQueue and WeakMap cover what arrays do poorly. Interviewers ask when a structure beats a plain array.

part ofPHPoverview, primer and where to startread it →
on this pageshow

explore

questions

6

In PHP, how do you make your own collection class usable in foreach, and when do you implement Iterator versus IteratorAggregate?

level: juniorimportance: must knowfreq 45%

answer

  1. Traversable is only a marker
  2. five methods versus one
  3. getIterator(): Traversable
  4. new ArrayIterator($this->items)
  5. #[\ReturnTypeWillChange] since 8.1

basics

~20 s

Implement IteratorAggregate and return an iterator such as new ArrayIterator($this->items) from getIterator(), or implement Iterator's five cursor methods yourself when traversal needs custom logic. Both extend Traversable, which foreach recognises; one class cannot implement both.

solid answer

~40 s

`foreach` walks any object that is `Traversable`, but a concrete userland class cannot implement `Traversable` on its own: it implements `Iterator` or `IteratorAggregate`. `Iterator` makes the object its own cursor, with `rewind()`, `valid()`, `current()`, `key()` and `next()`. `IteratorAggregate` has one method, `getIterator(): Traversable`, which hands `foreach` a separate iterator — for an array-backed collection simply `return new ArrayIterator($this->items);`. I default to `IteratorAggregate`: less code, no position field to keep in sync, and each loop gets a fresh cursor. I write a real `Iterator` only when the stepping logic itself is the point, such as walking a linked structure or computing values lazily. In PHP 8.5 the methods should declare the interface's return types (`mixed`, `void`, `bool`); leaving them off has raised a deprecation since 8.1.

code

php · 18 lines
php
<?php
declare(strict_types=1);

final class TicketList implements IteratorAggregate
{
    /** @param list<string> $tickets */
    public function __construct(private array $tickets = []) {}

    public function getIterator(): Iterator
    {
        return new ArrayIterator($this->tickets);
    }
}

$list = new TicketList(['T-1', 'T-2']);
foreach ($list as $i => $ticket) {
    echo "$i: $ticket\n"; // 0: T-1, then 1: T-2
}

go deeper

for a junior

Know that foreach needs Iterator or IteratorAggregate, name the five Iterator methods, and write the one-line getIterator() that returns new ArrayIterator($this->items).

for a middle

Explain the call order foreach uses, why IteratorAggregate gives each loop its own cursor, and the tentative return types that became deprecation notices in PHP 8.1.

for a senior

Choose the interface by where cursor state should live, accept iterable in consuming functions, and reach for a hand-written Iterator only when lazy or custom stepping earns it.

for a principal

Frame collection classes as an API boundary: exposing only iteration lets storage change freely, while leaking arrays couples every caller to one representation.

## What foreach does with an object In PHP, `foreach` accepts arrays and objects. For an object that is **not** `Traversable`, it walks the object's **visible properties**, which is almost never what a collection class wants. For an object that **is** `Traversable`, it asks the object for its elements instead. `Traversable` is an empty marker interface. A concrete userland class cannot implement it on its own (an abstract class or an interface may, leaving the choice to its subclasses); the engine rejects the class with a fatal error saying it must implement `Traversable` as part of either `Iterator` or `IteratorAggregate`. Those two interfaces are the real choices. ## Iterator: the object is the cursor `Iterator` declares five methods, each with a tentative return type in PHP 8.5: - `rewind(): void` — move to the first element - `valid(): bool` — is there a current element? - `current(): mixed` — the current value - `key(): mixed` — the current key - `next(): void` — advance A `foreach ($collection as $key => $value)` loop drives them in this order: 1. `rewind()` once, at the start of the loop. 2. `valid()`; if it returns `false`, the loop ends. 3. `current()`, and `key()` only when the loop asks for a key. 4. The loop body runs. 5. `next()`, then back to step 2. Because the position lives inside the object, you must keep a field such as `$this->position` in sync by hand, and two nested `foreach` loops over the **same** `Iterator` object share that one position. ## IteratorAggregate: hand over an iterator `IteratorAggregate` declares a single method, `getIterator(): Traversable`. `foreach` calls it once per loop and iterates whatever comes back. For a collection backed by an array, the usual body is one line: ```php public function getIterator(): Iterator { return new ArrayIterator($this->items); } ``` `ArrayIterator` is the SPL class that wraps an array and implements `Iterator` for it. Because `getIterator()` builds a new iterator on each call, every loop gets its own cursor, and the collection class carries no iteration state at all. The method may also return a generator or another object's iterator; anything `Traversable` is accepted. ## Choosing between them | | `Iterator` | `IteratorAggregate` | |---|---|---| | Methods to write | five | one | | Cursor state | inside the collection object | in the returned iterator | | Nested loops over one object | share one position | independent | | Typical use | custom stepping logic, lazily computed elements | array-backed or delegating collections | Most domain collections — a list of order lines, a set of permissions — hold an array internally, so `IteratorAggregate` plus `ArrayIterator` is the idiomatic answer. Implement `Iterator` directly when producing the next element is real work you want to control step by step, for example walking a linked node structure, paging through a resource, or computing values on demand. ## Details that come up in interviews - **Return types.** Since PHP 8.1 the internal interfaces carry *tentative* return types. An implementation that omits them triggers a deprecation notice ("Return type of … should either be compatible with …") unless the method carries `#[\ReturnTypeWillChange]`, which exists only to ease migration. New code declares `mixed`, `void` and `bool`. - **Not both.** A class cannot implement `Iterator` and `IteratorAggregate` at the same time; the engine refuses it at declaration. - **No foreach by reference.** `foreach ($collection as &$item)` on an iterator object throws `Error` with "An iterator cannot be used with foreach by reference". - **Typing parameters.** A function that only needs to loop should accept `iterable` (arrays or `Traversable`), so callers can pass either an array or your collection. - **Other capabilities are separate interfaces.** Being foreach-able does not make an object countable or indexable; those come from other interfaces, implemented alongside. ## A worked shape A `TicketList` that stores tickets in a private array implements `IteratorAggregate`, returns `new ArrayIterator($this->tickets)`, and can be passed straight to `foreach`, to `iterator_to_array()`, or to any function typed `iterable`. Callers never see the array, and the class can later switch its storage without changing a single loop. If the same class later needs to produce tickets lazily — say, reading them in pages — only `getIterator()` changes: it can return a hand-written `Iterator` that fetches the next page inside `next()`, and every `foreach` in the codebase keeps working. That is the practical payoff of hiding iteration behind one of these two interfaces rather than exposing a public array property: the loop is the contract, and the representation behind it stays private.

  • What does foreach do with an object that implements neither interface?
    It iterates the object's properties that are visible from the calling scope: public ones from outside the class, private and protected ones too when the loop runs inside it. That exposes the object's internals and usually is not what a collection wants, which is why collection classes implement `IteratorAggregate` or `Iterator`.
  • Why do two nested foreach loops over the same Iterator object misbehave, but not over an IteratorAggregate?
    An `Iterator` keeps its position in the object, so the inner loop's `rewind()` and `next()` move the cursor the outer loop is also using; the outer loop typically ends after its first pass. An `IteratorAggregate` returns a new iterator from `getIterator()` for each loop, so each loop has its own cursor.
  • When is implementing Iterator directly worth the extra code?
    When producing the next element is real work: walking a linked structure, reading records one at a time, or computing values lazily so the full set never sits in memory. Then the five methods let you control exactly what happens on each step, instead of building an array first just to wrap it.

saying these in an interview costs you the question

  • Implementing Traversable directly on a concrete class
  • Implementing Iterator for a class that just wraps an array
  • Believing foreach on any object throws unless it is Traversable
  • Leaving Iterator methods untyped in PHP 8.5 code
  • Assuming key() is called on every iteration even without a key variable
open as a page

In PHP, what does iterator_to_array() do with keys by default, and when does it silently lose elements?

level: middleimportance: should knowfreq 28%

basics

~20 s

iterator_to_array() copies every element into an array and, because $preserve_keys defaults to true, uses the iterator's keys. When two elements share a key the later one overwrites the earlier; pass false to get a list of all values.

open as a page

In PHP, how would you serve support tickets by severity with SplPriorityQueue, and what ordering does it not guarantee?

level: middleimportance: should knowfreq 30%

basics

~20 s

Call insert($ticket, $severity) and extract() in a loop: SplPriorityQueue is a max-heap, so the highest severity comes out first. Tickets with equal severity come out in undefined order, so add a sequence number to the priority when first-come-first-served matters.

open as a page

In PHP, why use SplQueue rather than array_push() and array_shift() for a FIFO work queue, and how does SplStack differ?

level: middleimportance: should knowfreq 32%

basics

~20 s

array_shift() renumbers all remaining keys, so each dequeue costs time proportional to the queue length. SplQueue is a doubly linked list with constant-time enqueue() and dequeue(). SplStack is the same list used LIFO, with push(), pop() and top().

open as a page

In a long-running PHP worker, when would you attach per-object data with WeakMap instead of SplObjectStorage, and why?

level: seniorimportance: should knowfreq 22%

basics

~20 s

SplObjectStorage holds strong references, so every object used as a key stays alive as long as the storage does, and a long-running worker leaks. WeakMap (PHP 8.0) holds keys weakly: an entry disappears when its object is destroyed elsewhere.

open as a page

In PHP, what does SplFixedArray give up compared with a plain array, and when is it actually worth using?

level: seniorimportance: nice to knowfreq 14%

basics

~20 s

SplFixedArray gives up string keys, appending with [], the array_* functions and copy-on-write value semantics. In return it holds exactly the requested number of integer-indexed slots, which can save memory and time for large numeric lists of known size.

open as a page