0

Iterator pattern — traversal without exposing internals

Intermediate5 min read·eng-04-007
compare

Concept

Iterator pattern provides a way to access elements of a collection sequentially without exposing the underlying representation. The collection and the traversal algorithm are separated.

PHP's built-in support: PHP has first-class iterator support:

  • Iterator interface: current(), key(), next(), rewind(), valid().
  • IteratorAggregate interface: getIterator() — returns an Iterator or Traversable.
  • Traversable: The base interface for foreach compatibility.
  • Generator: The simplest way to create a lazy iterator.

foreach compatibility: Any class implementing Iterator or IteratorAggregate can be used in a foreach loop. PHP calls rewind(), then loops valid() → current() → key() → next().

Generator as Iterator: function* iterator() (no such syntax — PHP uses yield):

php
function lazyRange(int $start, int $end): \Generator {
    for ($i = $start; $i <= $end; $i++) yield $i;
}

Generators are memory-efficient — only one value in memory at a time.

Custom Iterator use cases: Paginated API iteration (fetch next page when current page is exhausted), database cursors (one row at a time), lazy file reading, tree traversal.

Iterator vs array: Arrays load everything into memory. Iterators are lazy — they compute/fetch on demand. Use iterators for large data sets.

SPL Iterators: ArrayIterator, DirectoryIterator, RecursiveDirectoryIterator, LimitIterator, FilterIterator, CachingIterator.

Code Example

php
<?php
// Custom Iterator — paginated API
class PaginatedApiIterator implements \Iterator
{
    private array $currentPage = [];
    private int   $currentIndex = 0;
    private int   $page = 1;
    private bool  $hasMore = true;

    public function __construct(private readonly string $apiUrl) {}

    public function current(): array { return $this->currentPage[$this->currentIndex]; }
    public function key(): int       { return ($this->page - 2) * count($this->currentPage) + $this->currentIndex; }
    public function valid(): bool    { return $this->currentIndex < count($this->currentPage) || $this->hasMore; }

    public function next(): void
    {
        $this->currentIndex++;
        if ($this->currentIndex >= count($this->currentPage) && $this->hasMore) {
            $this->fetchPage();
        }
    }

    public function rewind(): void
    {
        $this->page         = 1;
        $this->currentIndex = 0;
        $this->hasMore      = true;
        $this->fetchPage();
    }

    private function fetchPage(): void
    {
        $response           = \Illuminate\Support\Facades\Http::get($this->apiUrl, ['page' => $this->page++]);
        $data               = $response->json();
        $this->currentPage  = $data['data'];
        $this->hasMore      = !empty($data['next_page_url']);
        $this->currentIndex = 0;
    }
}

// Generator-based Iterator — memory-efficient
function chunkFileLines(string $path, int $chunkSize = 1000): \Generator
{
    $handle = fopen($path, 'r');
    $chunk  = [];
    while (($line = fgets($handle)) !== false) {
        $chunk[] = trim($line);
        if (count($chunk) === $chunkSize) {
            yield $chunk;
            $chunk = [];
        }
    }
    if (!empty($chunk)) yield $chunk;
    fclose($handle);
}

// IteratorAggregate — simpler to implement
class NumberRange implements \IteratorAggregate
{
    public function __construct(
        private readonly int $start,
        private readonly int $end,
        private readonly int $step = 1,
    ) {}

    public function getIterator(): \Traversable
    {
        return (function() {
            for ($i = $this->start; $i <= $this->end; $i += $this->step) {
                yield $i;
            }
        })();
    }
}

$range = new NumberRange(1, 100, 5);
foreach ($range as $num) {
    echo $num . ' '; // 1, 6, 11, 16, ..., 96
}

// Eloquent cursor() — returns a Generator (lazy iterator)
// foreach (User::where('active', true)->cursor() as $user) {
//     processUser($user); // one User in memory at a time
// }