Generadores en PHP: yield, lazy evaluation y cómo ahorrar memoria

Los generadores de PHP implementan iteración lazy mediante la palabra clave yield: producen valores de uno en uno sin construir el array completo en memoria. Son la solución ideal para procesar ficheros de millones de líneas, resultados de base de datos muy grandes o secuencias de datos potencialmente infinitas.

Sintaxis básica: yield

<?php
function generarNumeros(int $inicio, int $fin): Generator
{
    for ($i = $inicio; $i <= $fin; $i++) {
        yield $i;
    }
}

$gen = generarNumeros(1, 5);
foreach ($gen as $num) {
    echo $num . ' '; // 1 2 3 4 5
}

// Un generador no ejecuta nada hasta que se itera
function conEfectoSecundario(): Generator
{
    echo "Inicion";
    yield 1;
    echo "Entre yieldn";
    yield 2;
    echo "Finn";
}

$g = conEfectoSecundario();  // nada se ejecuta aquí
$g->current();                // imprime "Inicio", produce 1
$g->next();                   // imprime "Entre yield", produce 2
$g->next();                   // imprime "Fin", termina
?>

Comparativa de memoria: array vs generador

<?php
// Con array: carga todo en memoria
function rangoArray(int $n): array
{
    return range(1, $n);
}

// Con generador: produce un número a la vez
function rangoGenerador(int $n): Generator
{
    for ($i = 1; $i <= $n; $i++) {
        yield $i;
    }
}

$inicio = memory_get_usage();
$arr = rangoArray(1_000_000);
echo 'Array: ' . round((memory_get_usage() - $inicio) / 1024 / 1024, 1) . ' MB';
unset($arr);

$inicio = memory_get_usage();
foreach (rangoGenerador(1_000_000) as $n) { /* procesar */ }
echo 'Generador: ' . round((memory_get_usage() - $inicio) / 1024 / 1024, 1) . ' MB';
// Array: ~32 MB  vs  Generador: ~0.5 MB
?>

Leer ficheros grandes línea a línea

<?php
function leerLineas(string $ruta): Generator
{
    $handle = fopen($ruta, 'r');
    if ($handle === false) {
        throw new RuntimeException("No se puede abrir: $ruta");
    }

    try {
        while (!feof($handle)) {
            $linea = fgets($handle);
            if ($linea !== false) {
                yield trim($linea);
            }
        }
    } finally {
        fclose($handle);
    }
}

// Procesar un CSV de millones de filas sin agotar la memoria
foreach (leerLineas('/datos/ventas.csv') as $linea) {
    if (empty($linea)) continue;
    $campos = str_getcsv($linea);
    // procesar cada fila...
}
?>

yield from: delegar en otro generador o iterable

<?php
function letras(): Generator
{
    yield 'a';
    yield 'b';
    yield 'c';
}

function numeros(): Generator
{
    yield 1;
    yield 2;
}

function todo(): Generator
{
    yield from letras();   // delega en el generador letras()
    yield from numeros();  // luego en numeros()
    yield from ['x', 'y', 'z']; // también funciona con arrays
}

foreach (todo() as $v) {
    echo $v . ' '; // a b c 1 2 x y z
}
?>

send() y getReturn()

<?php
function acumulador(): Generator
{
    $total = 0;
    while (true) {
        $valor = yield $total; // yield devuelve el total actual y recibe el siguiente
        if ($valor === null) break;
        $total += $valor;
    }
    return $total;
}

$gen = acumulador();
$gen->current();     // iniciar (total = 0)

$gen->send(10);      // total = 10
$gen->send(25);      // total = 35
$gen->send(5);       // total = 40
$gen->send(null);    // terminar

echo $gen->getReturn(); // 40
?>

Secuencias infinitas

<?php
function fibonacci(): Generator
{
    [$a, $b] = [0, 1];
    while (true) {
        yield $a;
        [$a, $b] = [$b, $a + $b];
    }
}

$fib = fibonacci();
$primeros10 = [];
for ($i = 0; $i < 10; $i++) {
    $primeros10[] = $fib->current();
    $fib->next();
}

echo implode(', ', $primeros10);
// 0, 1, 1, 2, 3, 5, 8, 13, 21, 34
?>

La documentación oficial de generadores en PHP detalla la interfaz Generator, los métodos send(), throw(), getReturn() y el comportamiento de las claves devueltas por yield.

COMPARTE ESTE ARTÍCULO

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