MWCodebymw.de ↗
PHP

Generatoren in PHP – eine 2-GB-Datei lesen, ohne den Speicher zu sprengen

file() lädt die ganze Datei in ein Array – bei großen Dateien ist danach der Speicher voll und das Skript tot. Mit yield gibst du Zeile für Zeile heraus und brauchst konstant wenig Speicher, egal wie groß die Datei ist.

Der Klassiker beim Import einer Kundenliste:

// ⚠️ lädt alles in den Speicher
foreach (file('export.csv') as $zeile) {
    verarbeite($zeile);
}

Bei 500 Zeilen ist das völlig in Ordnung. Bei 500.000 Zeilen bekommst du „Allowed memory size exhausted" — und die Versuchung, einfach memory_limit hochzudrehen. Das verschiebt das Problem nur bis zur nächsten, größeren Datei.

Die Lösung heißt yield

function zeilen(string $pfad): Generator
{
    $fh = fopen($pfad, 'rb');
    if ($fh === false) {
        throw new RuntimeException('Datei nicht lesbar: ' . $pfad);
    }
    try {
        while (($zeile = fgets($fh)) !== false) {
            yield rtrim($zeile, "\r\n");
        }
    } finally {
        fclose($fh);          // läuft auch, wenn die Schleife abgebrochen wird
    }
}

foreach (zeilen('export.csv') as $zeile) {
    verarbeite($zeile);
}

Von außen sieht das aus wie ein Array. Innen ist es das Gegenteil: Die Funktion läuft nur bis zum ersten yield, gibt den Wert heraus und friert ein. Beim nächsten Schleifendurchlauf macht sie genau dort weiter. Im Speicher liegt immer nur eine Zeile.

Das finally ist dabei kein Beiwerk. Bricht die Schleife mit break ab, wird der Generator eingesammelt und finally läuft — die Datei bleibt nicht offen.

Mit Schlüsseln – zum Beispiel für CSV

yield kann auch Schlüssel-Wert-Paare herausgeben:

function csvZeilen(string $pfad): Generator
{
    $fh = fopen($pfad, 'rb');
    $kopf = fgetcsv($fh, 0, ';');
    try {
        $nr = 1;
        while (($werte = fgetcsv($fh, 0, ';')) !== false) {
            yield $nr++ => array_combine($kopf, $werte);
        }
    } finally {
        fclose($fh);
    }
}

foreach (csvZeilen('kunden.csv') as $nr => $kunde) {
    echo $nr . ': ' . $kunde['name'] . PHP_EOL;
}

Jede Zeile kommt als fertiges assoziatives Array an — und trotzdem liegt nie mehr als eine davon im Speicher.

Generatoren lassen sich verketten

Das ist der Teil, der richtig Spaß macht: Ein Generator kann aus einem anderen lesen, ohne dass irgendwo eine vollständige Liste entsteht.

function nurAktive(iterable $kunden): Generator
{
    foreach ($kunden as $k) {
        if (($k['status'] ?? '') === 'aktiv') {
            yield $k;
        }
    }
}

foreach (nurAktive(csvZeilen('kunden.csv')) as $kunde) {
    // …
}

Drei Schritte hintereinander, konstanter Speicherverbrauch. Mit array_filter hättest du dazwischen zweimal die komplette Liste im RAM.

Wo Generatoren nicht passen

Sie sind einmalig durchlaufbar. Ein zweites foreach über denselben Generator wirft einen Fehler — die Funktion ist ja schon durch. Und count() funktioniert nicht, weil niemand vorher weiß, wie viele Elemente noch kommen. Wenn du die Daten mehrfach brauchst oder zählen musst, ist ein Array die richtige Wahl.

Alles andere — Importe, Exporte, Log-Auswertungen, Zeilen aus einer API-Antwort — läuft bei mir über Generatoren. Es kostet keine zusätzliche Zeile Code und nimmt eine ganze Sorte Problem aus dem Projekt.

Du hast einen Import, der bei großen Dateien wegbricht? Das ist fast immer genau das hier. Schreib mir.

Quellen

#PHP#Generatoren#yield#Speicher#CSV

Du brauchst mehr als ein Snippet?

Ich entwickle Android-Apps in Kotlin und moderne Websites für Selbstständige und kleine Unternehmen — von der ersten Idee bis zum Release.

Projekt anfragen →