Iterator Pattern
debt(d7/e5/b3/t5)
Closest to 'only careful code review or runtime testing' (d7). The detection_hints indicate automated detection is 'no' and the code pattern — a custom collection class without Iterator interface forcing callers to use getItems() then foreach — requires a human reviewer to spot. PHPStan (the listed tool) can catch type mismatches but won't flag a missing Iterator implementation as a structural problem. The misuse silently works but carries hidden costs.
Closest to 'touches multiple files / significant refactor in one component' (e5). The quick_fix describes implementing five Iterator interface methods (current, key, next, rewind, valid) or refactoring to a generator. However, callers using getItems()-then-foreach patterns may be spread across multiple files, and fixing rewind() bugs or switching from direct arrays to a proper Iterator requires updating both the collection class and potentially its consumers.
Closest to 'localised tax' (b3). The pattern applies per collection class. Once implemented, it mostly affects that one component and its direct callers. It doesn't define the system's shape, but it does impose a small ongoing tax on maintainers who must understand the Iterator contract and remember rewind() semantics for each custom collection.
Closest to 'notable trap (a documented gotcha most devs eventually learn)' (t5). The misconception field confirms the primary trap: developers believe foreach makes the Iterator pattern unnecessary, missing that implementing the interface enables custom objects to participate in foreach. The common_mistakes reinforce this — rewind() not resetting state (second foreach yields nothing) and not using IteratorAggregate or generators when simpler are well-known but non-obvious gotchas.
Also Known As
TL;DR
Explanation
The Iterator pattern defines traversal behaviour (current(), key(), next(), rewind(), valid()) separately from the collection, allowing the same collection to be traversed in multiple ways. PHP builds this into the language: foreach works on any class implementing Iterator or IteratorAggregate. SPL provides specialised iterators: ArrayIterator, RecursiveIteratorIterator, LimitIterator, FilterIterator, and AppendIterator. Custom iterators enable lazy evaluation — a DatabaseIterator fetches rows one at a time rather than loading all into memory. Generators (yield) provide a simpler alternative for most custom iteration needs.
Common Misconception
Why It Matters
Common Mistakes
- Loading an entire dataset into memory when a lazy iterator would do — a database cursor or generator is more efficient.
- Not implementing rewind() correctly — a second foreach produces no results because the iterator is exhausted.
- Implementing Iterator directly when PHP's IteratorAggregate is simpler for wrapping an existing traversable.
- Not using yield in generators when implementing lazy sequences — generators are PHP's lightweight iterator.
Code Examples
// Loading 1M rows into memory instead of iterating lazily:
function getAllOrders(): array {
return $this->db->query('SELECT * FROM orders')->fetchAll(); // Loads everything
}
foreach (getAllOrders() as $order) { /* processes one at a time but fetched all */ }
// Iterator provides sequential access without exposing internal structure
class UserCursor implements Iterator {
private int \$position = 0;
public function __construct(private array \$users) {}
public function current(): User { return \$this->users[\$this->position]; }
public function key(): int { return \$this->position; }
public function next(): void { \$this->position++; }
public function rewind(): void { \$this->position = 0; }
public function valid(): bool { return isset(\$this->users[\$this->position]); }
}
foreach (new UserCursor(\$users) as \$user) {
\$user->notify(\$message);
}
// Generator alternative — memory efficient for large datasets:
function userStream(PDO \$pdo): \Generator {
\$stmt = \$pdo->query('SELECT * FROM users');
while (\$row = \$stmt->fetch()) yield \$row;
}