SPL — Standard PHP Library: Interfaces, Iterators, and Data Structures
Understand the SPL's three pillars — its core interfaces (Traversable, Iterator, IteratorAggregate, Countable, ArrayAccess, RecursiveIterator, OuterIterator), its ready-made iterator classes (ArrayIterator, ArrayObject, FilterIterator, CallbackFilterIterator, RecursiveIteratorIterator, the DirectoryIterator family, GlobIterator, RegexIterator), and its specialized data structures (SplFixedArray, SplDoublyLinkedList/SplStack/SplQueue, SplHeap/SplMinHeap/SplMaxHeap/SplPriorityQueue, SplObjectStorage) — well enough to reach for the right built-in tool instead of reimplementing a stack, queue, or filtered iterator from scratch, and to reason correctly about each structure's time and memory behavior.
SPL — the Standard PHP Library — ships with every PHP install and has two distinct jobs: a small family of interfaces that let foreach, count(), and array-bracket syntax work uniformly on custom objects, not just plain arrays; and a family of purpose-built, C-implemented data structures (stacks, queues, heaps, fixed arrays, an object-keyed map) that behave more predictably — and often more efficiently — for their specific job than a plain PHP array pressed into service as if it were that structure. None of this replaces plain arrays for everyday code; SPL earns its place exactly when you need a structure's specific behavioral guarantee or are implementing a genuinely custom iterable type.
Why SPL exists: a standard vocabulary for iteration and data structures
Before SPL, 'making an object behave like an array' meant informally agreeing on method names (get(), has(), each()) with no way for generic code — foreach, count(), array-bracket access — to recognize and use them automatically. SPL's interfaces fix that by giving PHP itself a small set of contracts it checks for: implement the right interface and the language's own built-in syntax starts working on your object for free.
SPL's data-structure classes solve a related but separate problem: a plain PHP array is always, under the hood, an ordered hash map — flexible, but not the most efficient shape for every job. A stack that only ever needs push/pop at one end, a fixed-size numeric buffer, or a structure that always needs its minimum element next are all better served by a structure built specifically for that access pattern, which SPL provides ready-made and implemented in C for speed.
The core interfaces: Traversable, Iterator, IteratorAggregate
Traversable is a marker interface with no methods of its own — you never implement it directly. Instead you implement either Iterator or IteratorAggregate, and PHP recognizes both as satisfying Traversable, which is what actually makes foreach willing to accept an object instead of only a plain array.
Iterator requires five methods — current(), key(), next(), rewind(), valid() — giving you full, explicit control over every step of iteration; foreach calls them directly in a fixed sequence. IteratorAggregate requires just one — getIterator(): Iterator|Traversable — and delegates the real iteration work to whatever it returns, often a plain ArrayIterator wrapping an internal array, or a generator. IteratorAggregate is almost always simpler to implement correctly and is the usual first choice; hand-implementing Iterator directly is worth it mainly when you need fine-grained control over the iteration state itself (lazy computation per-step, custom rewind behavior).
• Traversable — marker interface; satisfied by implementing either Iterator or IteratorAggregate, never directly. • Iterator — current()/key()/next()/rewind()/valid(); full manual control, more code. • IteratorAggregate — a single getIterator() method; usually just delegates to ArrayIterator or a generator.
Countable and ArrayAccess: making objects behave like arrays
Countable requires one method, count(): int, which is what makes the built-in count($obj) function work on your object instead of only on arrays. ArrayAccess requires four — offsetExists(), offsetGet(), offsetSet(), offsetUnset() — which is what makes $obj[$key], isset($obj[$key]), and unset($obj[$key]) syntax work on an object exactly as it would on an array, while still letting you validate, transform, or lazily compute values on access.
A class implementing Iterator/IteratorAggregate, Countable, and ArrayAccess together behaves, from the outside, almost indistinguishably from a real array — foreach, count(), and $obj[...] all work — while internally you retain full control over storage, validation, and computed values. This is exactly the combination SPL's own ArrayObject implements, and the pattern most custom 'collection' classes reach for.
RecursiveIterator and OuterIterator: trees and wrapped iterators
RecursiveIterator extends Iterator with two extra methods, hasChildren(): bool and getChildren(): RecursiveIterator, letting an iterator describe a tree — nested arrays, directory structures — instead of a flat sequence. Iterating a RecursiveIterator directly only visits its top level; actually descending into and flattening the children requires wrapping it in RecursiveIteratorIterator.
OuterIterator is implemented by any iterator that wraps another one — FilterIterator, CallbackFilterIterator, RecursiveIteratorIterator itself — and exposes getInnerIterator() to reach the wrapped iterator underneath. This is the interface that makes iterator composition (layering several small decorators around one base iterator) a standard, recognizable pattern rather than an ad hoc convention.
Ready-made iterators: ArrayIterator, ArrayObject, and the FilterIterator family
ArrayIterator wraps a plain array behind the Iterator interface, letting you pass it around as an object, type-hint it as Iterator/Traversable, and advance it independently of any single foreach loop. ArrayObject goes further, wrapping an array behind Iterator, Countable, and ArrayAccess simultaneously — essentially turning a plain array into a full collection object — and, importantly, its getIterator() produces a fresh ArrayIterator on each call, so unlike a Generator, an ArrayObject can be iterated more than once.
FilterIterator is an abstract base class: extend it and implement accept(): bool to lazily filter an inner iterator's values, one at a time, without materializing an intermediate array. CallbackFilterIterator does the same job without requiring a subclass — you pass a callback directly — which is usually the faster path for a one-off filter. RecursiveIteratorIterator is the class that actually flattens a RecursiveIterator (a nested array, a directory tree) into a normal, flat, foreach-able sequence, with modes like the default LEAVES_ONLY (only leaf values appear) or SELF_FIRST (a parent node appears before its children).
Filesystem iterators: DirectoryIterator, FilesystemIterator, RecursiveDirectoryIterator, GlobIterator, RegexIterator
DirectoryIterator gives a lightweight, foreach-able listing of one directory's entries, each exposed as an SplFileInfo-like object (isDir(), isFile(), getFilename(), and more) — but it includes the '.' and '..' pseudo-entries every directory has, which calling code has to remember to skip. FilesystemIterator refines DirectoryIterator with better default flags (skipping dots automatically, among others) and is the more commonly recommended base for new code.
RecursiveDirectoryIterator is the RecursiveIterator version of FilesystemIterator — its hasChildren() returns true for subdirectories, so wrapping it in RecursiveIteratorIterator produces a full recursive walk of an entire directory tree without any manual recursion. GlobIterator shares the same interface but is constructed from a glob pattern (like '*.php') instead of a plain directory path, combining glob-style matching with the iterator interface. RegexIterator is another OuterIterator decorator that filters any inner iterator's values (or keys) against a regular expression — commonly wrapped around a RecursiveIteratorIterator(RecursiveDirectoryIterator(...)) chain to build 'find every file matching this pattern' tools that never load the whole tree into an array first.
Linear data structures: SplFixedArray, SplDoublyLinkedList, SplStack, SplQueue
SplFixedArray is a fixed-size, integer-indexed array allocated up front, with no hash-table overhead — unlike a normal PHP array, which is always an ordered hash map internally, regardless of whether its keys happen to be sequential integers. For a large, purely numeric, fixed-length collection, this means meaningfully less memory and predictable O(1) indexed access; the trade-off is you must know (or set) its size in advance, and it supports only integer keys.
SplDoublyLinkedList is a genuine doubly linked list, giving O(1) push/pop/shift/unshift at both ends, and it can be iterated in either direction via setIteratorMode(IT_MODE_LIFO or IT_MODE_FIFO). SplStack and SplQueue are thin subclasses of it that simply fix the iteration direction and provide a more intention-revealing API — push()/pop() for a LIFO stack, enqueue()/dequeue() for a FIFO queue. Pushing or popping at the ends of any of these three is O(1); by contrast, array_shift()/array_unshift() on a plain PHP array is O(n), because every remaining element's numeric index has to be renumbered — the concrete, measurable reason to reach for SplQueue over an array used as a queue in a hot path.
Heaps and priority queues: SplHeap, SplMinHeap, SplMaxHeap, SplPriorityQueue
SplHeap is an abstract base: extend it and implement compare(mixed $a, mixed $b): int (with the same contract as a usort() comparator) to define a custom ordering, and it gives you O(log n) insertion and O(log n) extraction of whichever element compare() ranks highest, backed by a real binary heap rather than re-sorting on every insert. SplMinHeap and SplMaxHeap are ready-made subclasses with compare() already defined for ascending/descending numeric order, so most everyday cases need no subclassing at all.
SplPriorityQueue is a heap keyed by a separate priority value supplied at insert($value, $priority) time, rather than by comparing the values themselves — ideal for task schedulers and graph algorithms like Dijkstra's or A* that repeatedly need 'the currently highest-priority pending item, right now.' By default it extracts by priority alone; setExtractFlags() lets you additionally retrieve the associated value, or both together as an array. All of these beat re-sorting a plain array on every insert: sorting a whole array for every push is O(n log n) per insertion, while a heap's insert and extract are each O(log n) — a difference that becomes dramatic once thousands of inserts are interleaved with extractions.
SplObjectStorage: a map keyed by object identity
SplObjectStorage lets objects themselves be used as map keys, compared purely by identity — not by any property or __toString() representation — something a plain PHP array cannot do at all, since array keys must be int|string. It can also attach arbitrary data to each stored object via attach($object, $data), effectively giving you a Map<object, mixed>.
It implements Countable, Iterator, and ArrayAccess together, so count($storage), foreach ($storage as $obj) { $storage->getInfo(); }, and $storage[$obj] = $data / isset($storage[$obj]) / unset($storage[$obj]) syntax are all interchangeable ways of working with it. Classic use cases: tracking which objects have already been visited during a graph or tree traversal (avoiding infinite loops on cyclic structures) without adding a 'visited' flag property to the objects themselves; an observer pattern's registered listener list, each optionally paired with per-listener configuration data; and caching a computed result keyed by the exact object instance it was computed for.
Custom iterators and iterator composition
Because Iterator, IteratorAggregate, RecursiveIterator, and OuterIterator are all standard, publicly documented interfaces, a fully custom iterator you write for a domain-specific traversal — walking a linked structure, paging through an API one page at a time — composes cleanly with every SPL decorator, exactly as if it were a built-in class: wrap it in a FilterIterator, feed it into a RecursiveIteratorIterator, filter it with a RegexIterator.
Iterator composition means stacking small, single-purpose iterators — one that filters, one that limits, one that flattens, one that transforms — around a base source, all still fully lazy, rather than writing one large loop that does everything at once. This mirrors the generator-pipeline pattern from the Generators lesson, expressed as composed objects instead of chained generator functions — and, unlike a generator-based pipeline, an object-based one built on rewindable pieces (like ArrayIterator or ArrayObject) can often be iterated more than once.
Code Examples
Implementing IteratorAggregate, Countable, and ArrayAccess together turns ProductCollection into an object that works with foreach, count(), and $products[0] array syntax simultaneously — while getIterator() delegates the real iteration work to a plain ArrayIterator, so ProductCollection itself never has to hand-implement current()/key()/next()/valid()/rewind(). Expected result: 2 Keyboard 0: Keyboard $49.99 1: Mouse $19.99 public function getIterator(): Iterator { return new ArrayIterator($this->items); }
SplStack's push()/pop() give O(1) LIFO behavior perfectly suited to bracket-matching, while the same underlying SplDoublyLinkedList used as an SplQueue gives O(1) FIFO enqueue()/dequeue() — both avoid the O(n) re-indexing cost that array_shift()/array_unshift() would incur if a plain array were used as a queue instead. Expected result: bool(true) bool(false) processing: send-welcome-email processing: generate-invoice processing: sync-inventory
$stack->push($char); /* ... */ $stack->pop();
SplPriorityQueue is a binary heap keyed by the priority argument to insert(), not by the value itself — extract() always removes and returns the currently highest-priority item in O(log n), so tasks come out strictly in priority order (10, 8, 3, 1) regardless of insertion order, without ever sorting the full list. Expected result: handle security patch send password reset email rotate logs restart cache
$scheduler->insert('handle security patch', 10);
SplObjectStorage::attach()/contains() key purely on object identity, so even though this graph has cycles (a -> b -> a, and c -> a), each Node is visited and counted exactly once — a plain array can't be keyed by an object at all, and SplObjectStorage does this job with a purpose-built API that can also, via attach($obj, $data), carry per-object data alongside the identity check for free. Expected result: 3 if (!$visited->contains($neighbor)) { $visited->attach($neighbor); /*... */ }
RecursiveArrayIterator turns a nested array into a RecursiveIterator, RecursiveIteratorIterator flattens it into one linear sequence of leaf values (skipping the intermediate 'app'/'db' nodes in its default LEAVES_ONLY mode), and CallbackFilterIterator then lazily keeps only the string leaves — three ordinary iterator objects layered on top of each other, none of which ever produces an intermediate array, exactly the composition pattern a RecursiveDirectoryIterator + RegexIterator chain uses for filesystem searches. Expected result: name = SampleApp host = localhost name = sampleapp
$stringsOnly = new CallbackFilterIterator($flattened, fn (mixed $value): bool => is_string($value));
final class ProductCollection implements IteratorAggregate, Countable, ArrayAccess
{
/** @var array<int, array{name: string, price: float}> */
private array $items = [];
public function add(string $name, float $price): void
{
$this->items[] = ['name' => $name, 'price' => $price];
}
public function getIterator(): Iterator
{
return new ArrayIterator($this->items);
}
public function count(): int
{
return count($this->items);
}
public function offsetExists(mixed $offset): bool
{
return isset($this->items[$offset]);
}
public function offsetGet(mixed $offset): mixed
{
return $this->items[$offset];
}
public function offsetSet(mixed $offset, mixed $value): void
{
if ($offset === null) {
$this->items[] = $value;
} else {
$this->items[$offset] = $value;
}
}
public function offsetUnset(mixed $offset): void
{
unset($this->items[$offset]);
}
}
$products = new ProductCollection();
$products->add('Keyboard', 49.99);
$products->add('Mouse', 19.99);
echo count($products) . PHP_EOL;
echo $products[0]['name
…function isBalanced(string $expression): bool
{
$stack = new SplStack();
foreach (str_split($expression) as $char) {
if ($char === '(') {
$stack->push($char);
} elseif ($char === ')') {
if ($stack->isEmpty()) {
return false;
}
$stack->pop();
}
}
return $stack->isEmpty();
}
var_dump(isBalanced('(a(b)c)'));
var_dump(isBalanced('(a(b)'));
$tasks = new SplQueue();
$tasks->enqueue('send-welcome-email');
$tasks->enqueue('generate-invoice');
$tasks->enqueue('sync-inventory');
while (!$tasks->isEmpty()) {
echo 'processing: ' . $tasks->dequeue() . PHP_EOL;
}$scheduler = new SplPriorityQueue();
$scheduler->insert('restart cache', 1);
$scheduler->insert('handle security patch', 10);
$scheduler->insert('rotate logs', 3);
$scheduler->insert('send password reset email', 8);
while (!$scheduler->isEmpty()) {
echo $scheduler->extract() . PHP_EOL;
}final class Node
{
/** @var Node[] */
public array $neighbors = [];
public function __construct(public readonly string $label) {}
}
function countReachable(Node $start): int
{
$visited = new SplObjectStorage();
$queue = new SplQueue();
$queue->enqueue($start);
$visited->attach($start);
while (!$queue->isEmpty()) {
$current = $queue->dequeue();
foreach ($current->neighbors as $neighbor) {
if (!$visited->contains($neighbor)) {
$visited->attach($neighbor);
$queue->enqueue($neighbor);
}
}
}
return count($visited);
}
$a = new Node('a');
$b = new Node('b');
$c = new Node('c');
$a->neighbors = [$b];
$b->neighbors = [$a, $c]; // cycle back to $a
$c->neighbors = [$a]; // another cycle
echo countReachable($a) . PHP_EOL;$config = [
'app' => ['name' => 'SampleApp', 'debug' => false],
'db' => ['host' => 'localhost', 'name' => 'sampleapp'],
];
$flattened = new RecursiveIteratorIterator(new RecursiveArrayIterator($config));
$stringsOnly = new CallbackFilterIterator($flattened, fn (mixed $value): bool => is_string($value));
foreach ($stringsOnly as $key => $value) {
echo "{$key} = {$value}" . PHP_EOL;
}Iterator composition: decorators wrapping a base iterator
Each layer is itself an Iterator/OuterIterator, so more decorators can be added without ever materializing the full file list into an array — the same composition principle applies to any RecursiveIterator, not just filesystem walks.
RecursiveDirectoryIterator (base: walks one directory's entries, can descend into subdirectories)
|
v wrapped by
RecursiveIteratorIterator (flattens the whole tree into one linear sequence)
|
v wrapped by
RegexIterator (keeps only entries whose pathname matches a pattern)
|
v
foreach ($files as $file) { ... } <- only matching files, walked lazily, one at a timeImportant Rules
Prefer IteratorAggregate + getIterator() over hand-implementing Iterator's five methods, unless you need direct control over iteration state itself.
Use SplStack/SplQueue instead of an array with array_shift()/array_unshift() whenever you need O(1) operations at the ends in a hot path.
Reach for SplHeap/SplMinHeap/SplMaxHeap/SplPriorityQueue instead of re-sorting an array on every insert when you repeatedly need the current min/max/highest-priority item.
SplObjectStorage is the only standard way to key a map by object identity — plain arrays cannot use objects as keys at all.
Compose iterators (FilterIterator/CallbackFilterIterator/RecursiveIteratorIterator/RegexIterator) around a base iterator instead of writing one large loop that filters, transforms, and flattens all at once.
SplFixedArray trades flexibility (no string keys, fixed size) for lower memory overhead on large, purely numeric collections.
Where this shows up
Doctrine ORM's identity map (its 'Unit of Work' tracking which entity objects are already managed) is built on SplObjectStorage-style object-identity keying; Symfony's Finder component composes RecursiveDirectoryIterator, RecursiveIteratorIterator, and its own filter iterators to implement flexible file-search rules without ever loading a whole directory tree into an array; job-queue libraries and background workers use SplQueue/SplPriorityQueue for FIFO and priority-based task dispatch; and graph/pathfinding libraries (Dijkstra's algorithm, A*) rely on SplPriorityQueue to always process the currently-cheapest frontier node next in O(log n).
while ($item = array_shift($queue)) { process($item); } // O(n) re-index on every shift, for a large $queue this is effectively O(n^2)
$queue = new SplQueue(); /*...enqueue items... */ while (!$queue->isEmpty()) { process($queue->dequeue()); } // O(1) dequeue regardless of size array_shift() on a plain PHP array is O(n), because every remaining element has to be renumbered after the first is removed; doing this in a loop over a large array is effectively O(n^2) overall. SplQueue::dequeue() is O(1) no matter how many items remain, because it's backed by a real doubly linked list rather than a renumbered array.
foreach (new RecursiveIteratorIterator(new RecursiveDirectoryIterator($dir)) as $file) { /*... */ } // walks into '.' and '..' too without the right flags foreach (new RecursiveIteratorIterator(new RecursiveDirectoryIterator($dir, FilesystemIterator::SKIP_DOTS)) as $file) { /*... */ }
RecursiveDirectoryIterator, by default, includes the '.' and '..' pseudo-entries every directory has, which can cause a recursive walk to revisit directories unexpectedly. Passing the FilesystemIterator::SKIP_DOTS flag to its constructor is the standard fix, and is almost always what a real directory walk needs.
What is the key structural difference between Iterator and IteratorAggregate? A. Iterator requires implementing current()/key()/next()/rewind()/valid() directly; IteratorAggregate requires only a single getIterator() method that delegates the real work B. IteratorAggregate is only usable with arrays, never with generators C. Iterator is deprecated in favor of IteratorAggregate D. They are functionally identical, just different names
A. Iterator requires implementing current()/key()/next()/rewind()/valid() directly; IteratorAggregate requires only a single getIterator() method that delegates the real work — Iterator gives full manual control over every step of iteration via its five required methods, called directly by foreach. IteratorAggregate requires only getIterator(), which typically delegates to something simpler, like an ArrayIterator or a generator — usually less code and the more common first choice.
Which interface makes $obj[$key] read/write syntax work on a custom object? A. Countable B. Traversable C. ArrayAccess D. OuterIterator
C. ArrayAccess — ArrayAccess (offsetExists/offsetGet/offsetSet/offsetUnset) is what makes bracket syntax — $obj[$key], isset($obj[$key]), unset($obj[$key]) — work on an object exactly as it would on a plain array.
Why is SplQueue::dequeue() faster than array_shift() for a large queue? A. SplQueue uses less memory per element than an array B. SplQueue is backed by a real doubly linked list, giving O(1) removal from the front, while array_shift() must renumber every remaining element's index, an O(n) operation C. array_shift() only works on associative arrays D. There is no actual performance difference between them
B. SplQueue is backed by a real doubly linked list, giving O(1) removal from the front, while array_shift() must renumber every remaining element's index, an O(n) operation — array_shift() on a plain array is O(n) because removing the first element forces every subsequent element's integer key to be renumbered. SplQueue (built on SplDoublyLinkedList) removes from the front in O(1), regardless of how many items remain.
What does SplPriorityQueue::insert($value, $priority) order elements by? A. The order they were inserted, ignoring $priority B. A comparison of $value against every other stored value C. The separate $priority argument supplied at insert time, not the value itself D. Alphabetical order of $value's string representation
C. The separate $priority argument supplied at insert time, not the value itself — SplPriorityQueue is a heap keyed by the priority argument passed to insert(), independent of the value being stored — extract() always removes the item with the currently highest priority, in O(log n).
What can SplObjectStorage do that a plain PHP array cannot? A. Store more than 2^32 elements B. Use an object itself as a map key, compared by identity, and optionally attach arbitrary data to each stored object C. Automatically sort its contents D. Guarantee insertion order across PHP versions
B. Use an object itself as a map key, compared by identity, and optionally attach arbitrary data to each stored object — PHP array keys must be int|string — an object can never be used directly as an array key. SplObjectStorage is specifically built to key by object identity and can attach arbitrary per-object data via attach($object, $data), which a plain array structurally cannot support.
What must a plain RecursiveIterator be wrapped in to actually flatten and walk into its children during iteration? A. ArrayObject B. RecursiveIteratorIterator C. SplObjectStorage D. CallbackFilterIterator
B. RecursiveIteratorIterator — Iterating a RecursiveIterator directly only visits its top level — hasChildren()/getChildren() describe how to descend, but RecursiveIteratorIterator is the class that actually performs that descent and produces a single, flat, foreach-able sequence.
Using SplStack, write a function reverseWords(string $sentence): string that reverses the order of words in a sentence (not the letters within each word) by pushing each word onto a stack and letting SplStack's default LIFO iteration order do the reversing.
function reverseWords(string $sentence): string { $stack = new SplStack(); foreach (explode(' ', $sentence) as $word) { $stack->push($word); } $result = []; foreach ($stack as $word) { $result[] = $word; } return implode(' ', $result); } echo reverseWords('the quick brown fox'); // fox brown quick the explode($sentence, ' ') to get the words, push() each onto an SplStack, then foreach over the stack directly — SplStack's default iterator mode is already LIFO, so no manual pop() loop is needed.