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
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 →Verwandte Snippets
Beträge und Zahlen in PHP formatieren – number_format, sprintf und Intl
1234.5 ist keine Anzeige, sondern ein Wert. Auf der Rechnung soll 1.234,50 € stehen, in der API 1234.50 und im Dateinamen 1234-50. Drei Aufgaben, drei Werkzeuge – und ein Fehler, den fast jeder einmal macht.
Benannte Argumente in PHP 8 – Schluss mit true, false, null, true
Ein Aufruf wie erstelle($a, true, false, null, true) sagt nichts darüber, was die Wahrheitswerte bedeuten. Mit benannten Argumenten steht es im Aufruf – und optionale Parameter lassen sich überspringen.
Reguläre Ausdrücke in PHP – benannte Gruppen statt $m[3]
Ein Treffer-Array aus Zahlen liest sich nach zwei Wochen wie eine Geheimschrift. Benannte Gruppen geben jedem Teil einen Namen – und preg_replace_callback ersetzt damit Dinge, die kein einfacher Ersetzungsstring kann.