The Iterator pattern defines sequential access to collection elements without exposing its internal structure. PHP implements it through the Iterator and IteratorAggregate interfaces. Generators (available since PHP 5.5) are syntactic sugar that simplifies writing iterators considerably – especially for lazily generating large datasets.
The Iterator interface in PHP
<?php
declare(strict_types=1);
class ProductCollection implements Iterator
{
private array $products = [];
private int $position = 0;
public function add(array $product): void
{
$this->products[] = $product;
}
public function current(): mixed
{
return $this->products[$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->products[$this->position]);
}
}
$collection = new ProductCollection();
$collection->add(['sku' => 'SKU-001', 'name' => 'Widget', 'price' => 29.99]);
$collection->add(['sku' => 'SKU-002', 'name' => 'Gadget', 'price' => 49.99]);
foreach ($collection as $key => $product) {
echo "{$key}: {$product['sku']} - {$product['name']}\n";
}
IteratorAggregate – simpler approach
<?php
declare(strict_types=1);
class OrderRepository implements IteratorAggregate
{
private array $orders = [];
public function add(array $order): void
{
$this->orders[] = $order;
}
public function getIterator(): ArrayIterator
{
return new ArrayIterator($this->orders);
}
public function pending(): ArrayIterator
{
$pending = array_filter(
$this->orders,
fn($o) => $o['status'] === 'pending'
);
return new ArrayIterator(array_values($pending));
}
}
Generator – iterator without boilerplate
A generator is a function with a yield statement. PHP automatically implements the Generator interface (which implements Iterator) for it. Useful when generating elements is expensive or the dataset is too large to hold in memory:
<?php
declare(strict_types=1);
// Infinite Fibonacci generator
function fibonacci(): Generator
{
[$a, $b] = [0, 1];
while (true) {
yield $a;
[$a, $b] = [$b, $a + $b];
}
}
// Stream large CSV without loading into memory
function readCsvRows(string $filePath): Generator
{
$handle = fopen($filePath, 'r');
if ($handle === false) {
throw new \RuntimeException("Cannot open: {$filePath}");
}
$headers = fgetcsv($handle);
try {
while (($row = fgetcsv($handle)) !== false) {
yield array_combine($headers, $row);
}
} finally {
fclose($handle);
}
}
// Processes 100k CSV rows with constant memory
foreach (readCsvRows('/tmp/products.csv') as $row) {
processProduct($row);
}
// yield from - delegate to another generator
function mergeGenerators(Generator ...$generators): Generator
{
foreach ($generators as $gen) {
yield from $gen;
}
}
// Generator with key => value
function indexedProducts(array $products): Generator
{
foreach ($products as $product) {
yield $product['sku'] => $product;
}
}
Generators and send() – two-way communication
<?php
function logger(): Generator
{
$messages = [];
while (true) {
$message = yield count($messages);
if ($message === null) break;
$messages[] = $message;
echo "[LOG] {$message}\n";
}
return $messages;
}
$log = logger();
$log->current(); // initialise
$log->send('Order #123 accepted');
$log->send('Payment confirmed');
$log->send('Shipping dispatched');
$log->send(null);
$allMessages = $log->getReturn();
Iterator in Magento 2
Magento\Framework\Data\Collection implements IteratorAggregate – which is why you can foreach over it. ResourceModel\Collection\AbstractCollection adds lazy loading from the database: the collection is actually loaded at the first iteration or getItems() call. For large datasets (reindexing, for example) Magento uses BatchProviderInterface which internally acts like a generator, fetching data page by page.
