Iterator e IteratorAggregate en PHP: hacer una clase iterable con foreach

PHP permite que cualquier clase sea recorrible con foreach implementando las interfaces Iterator o IteratorAggregate. La primera obliga a controlar el estado de la iteración dentro de la propia clase; la segunda delega la iteración en un objeto externo o un generador. Elegir una u otra depende de si la clase debe ser en sí misma el cursor de iteración o si conviene separarlos.

La interfaz Iterator

Iterator exige implementar cinco métodos: current(), key(), next(), rewind() y valid(). La clase mantiene un puntero interno que foreach maneja automáticamente.

<?php
class Coleccion implements Iterator
{
    private int $posicion = 0;

    public function __construct(private array $elementos) {}

    // Devuelve el elemento actual
    public function current(): mixed
    {
        return $this->elementos[$this->posicion];
    }

    // Devuelve la clave actual
    public function key(): int
    {
        return $this->posicion;
    }

    // Avanza al siguiente elemento
    public function next(): void
    {
        $this->posicion++;
    }

    // Vuelve al inicio
    public function rewind(): void
    {
        $this->posicion = 0;
    }

    // Indica si la posición actual es válida
    public function valid(): bool
    {
        return isset($this->elementos[$this->posicion]);
    }
}

$frutas = new Coleccion(['manzana', 'naranja', 'plátano']);

foreach ($frutas as $indice => $fruta) {
    echo "$indice: $frutan";
}
// 0: manzana
// 1: naranja
// 2: plátano
?>

La interfaz IteratorAggregate

IteratorAggregate solo exige el método getIterator(), que devuelve un objeto iterable. Es más sencilla de implementar y permite reutilizar iteradores existentes como ArrayIterator o generadores.

<?php
use ArrayIterator;

class CatalogProductos implements IteratorAggregate
{
    private array $productos = [];

    public function agregar(string $nombre, float $precio): void
    {
        $this->productos[] = compact('nombre', 'precio');
    }

    public function getIterator(): ArrayIterator
    {
        // Ordenar por precio antes de iterar
        $copia = $this->productos;
        usort($copia, fn($a, $b) => $a['precio'] <=> $b['precio']);
        return new ArrayIterator($copia);
    }
}

$catalogo = new CatalogProductos();
$catalogo->agregar('Teclado', 49.99);
$catalogo->agregar('Ratón', 29.99);
$catalogo->agregar('Monitor', 299.99);

foreach ($catalogo as $producto) {
    echo $producto['nombre'] . ': ' . $producto['precio'] . ' €n';
}
// Ratón: 29.99 €
// Teclado: 49.99 €
// Monitor: 299.99 €
?>

Iteración lazy con yield

Para conjuntos de datos grandes, calcular todos los elementos de golpe consume mucha memoria. Un generador con yield produce los elementos uno a uno y PHP los descarta tras procesarlos.

<?php
class SecuenciaNumerica implements IteratorAggregate
{
    public function __construct(
        private int $desde,
        private int $hasta,
        private int $paso = 1,
    ) {}

    public function getIterator(): Generator
    {
        for ($i = $this->desde; $i <= $this->hasta; $i += $this->paso) {
            yield $i; // produce el valor sin guardar todos en memoria
        }
    }
}

$pares = new SecuenciaNumerica(2, 1000000, 2);

$suma = 0;
foreach ($pares as $numero) {
    $suma += $numero;
}
echo $suma; // 250000500000 (suma de los pares del 2 al 1.000.000)
// Solo se guardó un número en memoria a la vez
?>

Paginador de BD que carga por lotes

Un caso de uso habitual es iterar sobre millones de filas de base de datos sin cargarlas todas en memoria. El paginador carga lotes de N filas y el foreach del código cliente no nota la diferencia.

<?php
class PaginadorBD implements IteratorAggregate
{
    public function __construct(
        private PDO    $pdo,
        private string $sql,
        private array  $params   = [],
        private int    $lote     = 1000,
    ) {}

    public function getIterator(): Generator
    {
        $offset = 0;

        do {
            $stmt = $this->pdo->prepare(
                $this->sql . " LIMIT {$this->lote} OFFSET $offset"
            );
            $stmt->execute($this->params);
            $filas = $stmt->fetchAll(PDO::FETCH_ASSOC);

            foreach ($filas as $fila) {
                yield $fila; // entregar fila a fila
            }

            $offset += $this->lote;
        } while (count($filas) === $this->lote);
    }
}

// Uso: iterar sobre todos los usuarios sin cargarlos en memoria
$paginador = new PaginadorBD($pdo, 'SELECT * FROM usuarios WHERE activo = 1', [], 500);

foreach ($paginador as $usuario) {
    procesarUsuario($usuario);
}
?>

La documentación oficial de la interfaz Iterator y la de IteratorAggregate detallan las firmas exactas de cada método, el tipo de retorno Traversable y cómo combinarlas con otras interfaces de la SPL como Countable o ArrayAccess.

COMPARTE ESTE ARTÍCULO

COMPARTIR EN FACEBOOK
COMPARTIR EN TWITTER
COMPARTIR EN LINKEDIN
COMPARTIR EN WHATSAPP