Funzioni generatrici in PHP: elaborare dati senza sprecare memoria

by theArchitect
SHARE
Funzioni generatrici in PHP: elaborare dati senza sprecare memoria
© Guida-HTML5.it

Introduzione

Quando una funzione deve restituire molti risultati, la soluzione più immediata consiste nel costruire un array e restituirlo alla fine. Questo approccio è semplice, ma può diventare inefficiente quando i dati sono numerosi: tutti gli elementi devono rimanere in memoria contemporaneamente.

PHP offre un’alternativa molto utile: le funzioni generatrici. Una funzione generatrice utilizza la parola chiave yield per produrre un valore alla volta. Invece di creare subito l’intero risultato, la funzione si mette in pausa dopo ogni valore e riprende soltanto quando il chiamante richiede l’elemento successivo.

Questo meccanismo è particolarmente adatto per:

  • leggere file di grandi dimensioni;
  • elaborare record provenienti da un database;
  • generare sequenze numeriche;
  • trasformare dati in modo progressivo;
  • ridurre il consumo di memoria durante l’elaborazione.

Una funzione che contiene almeno un’istruzione yield restituisce un oggetto di tipo Generator. L’oggetto può essere attraversato con un ciclo foreach, proprio come un array, ma i valori vengono calcolati soltanto quando servono.

Codice completo

<?php

/**
 * Legge un file CSV una riga alla volta.
 *
 * @param string $percorso Percorso del file CSV.
 * @return Generator<int, array<string, string>>
 */
function leggiProdotti(string $percorso): Generator
{
    $file = fopen($percorso, ´rb´);

    if ($file === false) {
        throw new RuntimeException(
            "Impossibile aprire il file: {$percorso}"
        );
    }

    // La prima riga contiene i nomi delle colonne.
    $intestazioni = fgetcsv($file);

    if ($intestazioni === false) {
        fclose($file);
        return;
    }

    while (($riga = fgetcsv($file)) !== false) {
        // Ignora righe vuote o incomplete.
        if (count($riga) !== count($intestazioni)) {
            continue;
        }

        // Associa ogni valore al nome della relativa colonna.
        yield array_combine($intestazioni, $riga);
    }

    fclose($file);
}

/**
 * Calcola il prezzo finale applicando l´IVA.
 */
function prezzoConIva(float $prezzo, float $aliquota = 22.0): float
{
    return $prezzo * (1 + $aliquota / 100);
}

try {
    foreach (leggiProdotti(__DIR__ . ´/prodotti.csv´) as $prodotto) {
        $prezzo = (float) $prodotto[´prezzo´];
        $prezzoFinale = prezzoConIva($prezzo);

        echo $prodotto[´nome´] . ´: ´;
        echo number_format($prezzoFinale, 2, ´,´, ´.´) . " euron";
    }
} catch (RuntimeException $errore) {
    echo ´Errore: ´ . $errore->getMessage();
}

Un possibile file prodotti.csv è il seguente:

nome,prezzo
Tastiera meccanica,79.90
Mouse wireless,34.50
Monitor 24 pollici,159.99

Spiegazione

La differenza tra return e yield

L’istruzione return termina immediatamente la funzione e restituisce un solo risultato. Una funzione generatrice, invece, può usare più istruzioni yield. Ogni chiamata produce un valore, poi l’esecuzione viene sospesa.

<?php

function contaFinoA(int $limite): Generator
{
    for ($numero = 1; $numero <= $limite; $numero++) {
        yield $numero;
    }
}

foreach (contaFinoA(3) as $numero) {
    echo $numero . "n";
}

Durante il ciclo, la funzione non calcola tutti i numeri in anticipo. Genera il numero successivo soltanto quando il ciclo ne ha bisogno. Questa caratteristica viene chiamata valutazione pigra, o lazy evaluation.

Il metodo send()

Un generatore può anche ricevere un valore dall’esterno tramite il metodo send(). È una funzionalità avanzata, utile quando la funzione deve collaborare con il codice chiamante durante l’elaborazione.

<?php

function riceviMessaggi(): Generator
{
    while (true) {
        $messaggio = yield ´Pronto a ricevere´;

        if ($messaggio === ´stop´) {
            return;
        }

        yield "Ricevuto: {$messaggio}";
    }
}

$generatore = riceviMessaggi();

echo $generatore->current() . "n";
$generatore->send(´Ciao´);
echo $generatore->current() . "n";

Nella maggior parte dei progetti, tuttavia, è sufficiente usare i generatori con foreach. Il codice risulta più leggibile e più semplice da mantenere.

Generatori e memoria

Supponiamo di dover elaborare un file con un milione di righe. Con file(), PHP caricherebbe l’intero file in memoria. Con fgets() all’interno di una funzione generatrice, invece, viene mantenuta in memoria soltanto la riga corrente e una piccola quantità di dati aggiuntivi.

<?php

function righeFile(string $percorso): Generator
{
    $file = fopen($percorso, ´rb´);

    if ($file === false) {
        throw new RuntimeException(´File non disponibile´);
    }

    while (($riga = fgets($file)) !== false) {
        yield rtrim($riga);
    }

    fclose($file);
}

foreach (righeFile(´access.log´) as $riga) {
    if (str_contains($riga, ´ERROR´)) {
        echo $riga . "n";
    }
}

Best practice

  • Usa i generatori per flussi di dati grandi: per pochi elementi, un array può essere più semplice e appropriato.
  • Indica il tipo di ritorno: usa Generator per rendere chiaro il comportamento della funzione.
  • Chiudi le risorse: quando utilizzi file o connessioni, assicurati che vengano chiusi al termine dell’elaborazione.
  • Gestisci gli errori: controlla sempre il risultato di fopen(), fgetcsv() e delle altre funzioni che lavorano con risorse esterne.
  • Non confondere yield con return: yield produce un elemento e sospende la funzione; return conclude l’intero generatore.
  • Mantieni il foreach leggibile: sposta le trasformazioni complesse in funzioni separate, evitando cicli difficili da comprendere.
  • Considera le chiavi: puoi usare la sintassi yield $chiave => $valore per associare una chiave a ogni elemento generato.

Riepilogo

Le funzioni generatrici permettono di produrre dati gradualmente mediante la parola chiave yield. Il risultato non è un array completo, ma un oggetto Generator attraversabile con foreach. Questo approccio riduce il consumo di memoria e rende efficiente l’elaborazione di file, sequenze e flussi di dati molto grandi.

Il vantaggio principale non è soltanto tecnico: separando la produzione dei dati dal loro consumo, il codice diventa più flessibile. Il chiamante può fermarsi in qualsiasi momento, elaborare un elemento alla volta e interrompere il flusso senza attendere la costruzione dell’intero risultato.

Approfondisci con risorse ufficiali

  • Manuale PHP sui generatori: documentazione ufficiale per comprendere yield, Generator e la gestione dell’iterazione.
  • Manuale PHP su foreach: riferimento per attraversare array e oggetti iterabili.
  • Manuale PHP sulle funzioni: sintassi, tipi di ritorno, eccezioni e comportamento delle funzioni.
  • Manuale PHP sulle funzioni di gestione dei file: informazioni su fopen(), fgets() e fgetcsv().

SHARE