Curso de PHP

Iteración de objetos en PHP: Iterator, IteratorAggregate y generadores

Por Víctor Peña · Publicado el

Iteración de objetos en PHP

Hola, ¿cómo están? Continuando con el curso de PHP, hoy veremos la iteración de objetos: cómo hacer que un objeto se pueda recorrer con foreach igual que un array.

¡Empecemos!

El comportamiento por defecto

Si aplicas foreach a un objeto sin más, PHP recorre sus propiedades públicas:

<?php
class Mascota
{
    public function __construct(
        public string $nombre = "Nupi",
        public string $tipo = "Perro",
        public int $edad = 3,
        private string $codigo = "MSC-001",
    ) {}
}

foreach (new Mascota() as $clave => $valor) {
    echo "$clave: $valor\n";
}

Salida:

nombre: Nupi
tipo: Perro
edad: 3

Fíjate en que $codigo no aparece: es privada y desde fuera no es visible. Ese comportamiento es útil de vez en cuando, pero rara vez es lo que quieres de verdad.

El caso real: un objeto que contiene una colección

Lo habitual es tener una clase que guarda una lista y querer recorrer esa lista, no las propiedades del objeto:

<?php
class Refugio
{
    private array $mascotas = [];

    public function agregar(string $mascota): void
    {
        $this->mascotas[] = $mascota;
    }
}

$refugio = new Refugio();
$refugio->agregar("Nupi");
$refugio->agregar("Michi");

foreach ($refugio as $mascota) {
    echo $mascota;   // no imprime nada: $mascotas es privada
}

Aquí es donde entran las interfaces de iteración.

IteratorAggregate: la forma sencilla

Es la solución más práctica y la que vas a usar el 90 % de las veces. Solo exige implementar un método:

<?php
class Refugio implements IteratorAggregate
{
    private array $mascotas = [];

    public function agregar(string $mascota): void
    {
        $this->mascotas[] = $mascota;
    }

    public function getIterator(): ArrayIterator
    {
        return new ArrayIterator($this->mascotas);
    }
}

$refugio = new Refugio();
$refugio->agregar("Nupi");
$refugio->agregar("Michi");

foreach ($refugio as $mascota) {
    echo $mascota . "\n";   // Nupi, Michi
}

ArrayIterator es una clase que trae PHP y que convierte un array en algo recorrible. Con seis líneas, el objeto se comporta como una colección sin exponer la propiedad privada.

Iterator: control total

Cuando necesitas controlar exactamente cómo avanza el recorrido, implementas Iterator, que exige cinco métodos:

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

    public function __construct(
        private int $desde,
        private int $hasta,
    ) {
        $this->posicion = $desde;
    }

    public function current(): int
    {
        return $this->posicion;
    }

    public function key(): int
    {
        return $this->posicion - $this->desde;
    }

    public function next(): void
    {
        $this->posicion++;
    }

    public function rewind(): void
    {
        $this->posicion = $this->desde;
    }

    public function valid(): bool
    {
        return $this->posicion <= $this->hasta;
    }
}

foreach (new Contador(5, 8) as $i => $valor) {
    echo "$i => $valor\n";
}

Los cinco métodos son las piezas del bucle:

Método Qué hace
rewind() Vuelve al inicio. Se llama una vez al empezar
valid() ¿Queda algo? Devuelve true o false
current() El valor actual
key() La clave actual
next() Avanza al siguiente

foreach llama a estos métodos por ti, en ese orden, en cada vuelta.

Cuándo usar cada uno: si tus datos ya están en un array, IteratorAggregate. Si se generan sobre la marcha o vienen de una fuente externa, Iterator.

Generadores: la opción moderna

Los generadores son la forma más limpia de crear secuencias, y resuelven además un problema serio de memoria.

Una función con yield devuelve valores de uno en uno, sin construir el array completo:

<?php
function contarHasta(int $limite): Generator
{
    for ($i = 1; $i <= $limite; $i++) {
        yield $i;
    }
}

foreach (contarHasta(5) as $numero) {
    echo $numero . " ";   // 1 2 3 4 5
}

Por qué importan de verdad

Compara estas dos formas de leer un archivo de un millón de líneas:

<?php
// Carga TODO el archivo en memoria
function leerTodo(string $archivo): array
{
    return file($archivo);
}

// Devuelve una línea a la vez
function leerLinea(string $archivo): Generator
{
    $manejador = fopen($archivo, 'r');

    while (($linea = fgets($manejador)) !== false) {
        yield $linea;
    }

    fclose($manejador);
}

La primera puede agotar la memoria del servidor. La segunda usa la misma memoria tanto para diez líneas como para diez millones, porque solo mantiene una línea a la vez.

Es una de esas cosas que no importan mientras pruebas con datos pequeños y se vuelven críticas en producción.

Generadores con clave

<?php
function listarMascotas(): Generator
{
    yield 'perro' => 'Nupi';
    yield 'gato' => 'Michi';
}

foreach (listarMascotas() as $tipo => $nombre) {
    echo "$tipo: $nombre\n";
}

Combinarlo con getIterator

Un generador puede ser el iterador de tu clase:

<?php
class Refugio implements IteratorAggregate
{
    private array $mascotas = ["Nupi", "Michi", "Toby"];

    public function getIterator(): Generator
    {
        foreach ($this->mascotas as $mascota) {
            yield strtoupper($mascota);
        }
    }
}

foreach (new Refugio() as $m) {
    echo $m . " ";   // NUPI MICHI TOBY
}

Contar los elementos

Si además quieres que count() funcione sobre tu objeto, implementa Countable:

<?php
class Refugio implements IteratorAggregate, Countable
{
    private array $mascotas = [];

    public function agregar(string $m): void
    {
        $this->mascotas[] = $m;
    }

    public function getIterator(): ArrayIterator
    {
        return new ArrayIterator($this->mascotas);
    }

    public function count(): int
    {
        return count($this->mascotas);
    }
}

$refugio = new Refugio();
$refugio->agregar("Nupi");

echo count($refugio);   // 1

Una clase que se recorre con foreach y se cuenta con count() se comporta, de cara a quien la usa, exactamente como un array. Y sin exponer sus datos internos.

Errores comunes

  • Implementar Iterator cuando bastaba IteratorAggregate. Cinco métodos en lugar de uno.
  • Olvidar rewind() o dejarlo vacío: el segundo foreach sobre el mismo objeto no recorre nada.
  • Intentar recorrer un generador dos veces. Se consumen: si necesitas repetir, conviértelo con iterator_to_array().
  • Cargar todo en memoria cuando un generador habría bastado.

Para cerrar

Hacer que tus objetos se puedan recorrer con foreach es lo que los integra con el resto del lenguaje. En la práctica: usa IteratorAggregate con ArrayIterator para colecciones normales, e yield cuando los datos sean muchos o se generen sobre la marcha.

En la siguiente lección veremos el polimorfismo.

Saludos y éxitos.

Norvic Software

Desarrollamos el software que tu empresa necesita

Somos una fábrica de software en Bolivia. Construimos sistemas a medida y aplicaciones móviles, y llevamos Inteligencia Artificial a las empresas que ya tienen un sistema funcionando.

Solicitar cotizaciónVer todos los servicios

Cotización sin costo · Respuesta directa por WhatsApp