Iterator pattern — traversal without exposing internals
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:
Iteratorinterface:current(),key(),next(),rewind(),valid().IteratorAggregateinterface:getIterator()— returns anIteratororTraversable.Traversable: The base interface forforeachcompatibility.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):
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
// 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
// }