Eine Protokolldatei soll um eine Zeile wachsen, nicht neu beginnen. Eine Lieferantenliste mit 800 MB soll verarbeitet werden, ohne dass der Arbeitsspeicher voll läuft. Beides sind Aufgaben, bei denen die kurzen Wege nicht mehr reichen und ein offener Zugriffspunkt auf die Datei gebraucht wird.
Das Bild zeigt, worum es geht: aus einer Datei wird ein Handle, und der Modus entscheidet, was damit erlaubt ist. Genau dort beginnt dieses Tutorial.
Was PHP fopen() macht
Die Signatur lautet fopen(string $filename, string $mode, bool $use_include_path = false, $context = null). Zurück kommt eine Stream-Ressource oder false. Diese Ressource ist der Handle, also der Zugriffspunkt, über den alle weiteren Funktionen arbeiten.
<?php
$fp = fopen('daten/notizen.txt', 'r');
if ($fp === false) {
exit('Datei laesst sich nicht oeffnen.');
}
$erste = fgets($fp);
echo $erste;
fclose($fp);
Wichtig ist der zweite Blick auf den Rückgabewert. PHP fopen wirft keine Ausnahme, wenn etwas schiefgeht, sondern liefert false und dazu eine Warnung. Ein try mit catch fängt hier also nichts ab. Geprüft wird der Rückgabewert, und zwar mit ===, weil eine Ressource in einem lockeren Vergleich unerwartete Ergebnisse liefern kann.
fopen() oder file_get_contents()? Die Entscheidungsregel
Diese Frage steht am Anfang jeder Dateiarbeit, deshalb kommt sie hier so weit vorn. file_get_contents() und file() holen den kompletten Inhalt in einem Zug und sind danach fertig. PHP fopen gibt stattdessen einen Handle zurück, der offen bleibt, bis er geschlossen wird. Genau deshalb lässt sich damit schrittweise lesen, mittendrin springen, sperren und weiterschreiben.
Die Arbeitsteilung ergibt sich daraus fast von selbst. Eine kleine Konfigurationsdatei, die vollständig gebraucht wird, holt man mit der Kurzform. Eine große Datei, ein paralleler Zugriff oder ein gezielter Teilbereich verlangen nach dem Handle. Wer eine Datei zeilenweise in ein Array einlesen will, findet in PHP file() für das zeilenweise Einlesen in ein Array den passenden kurzen Weg, und zum reinen Schreiben führt PHP file_put_contents() zum Schreiben und Anhängen schneller zum Ziel.
Die Modi von PHP fopen() in der Übersicht
Der Modus ist die Stelle, an der die teuren Fehler passieren. Ein einziger falscher Buchstabe löscht den Bestand einer Woche. Drei Fragen beantwortet jeder Modus: Wird die vorhandene Datei geleert? Wird sie angelegt, wenn sie fehlt? Und wo steht der Zeiger nach dem Öffnen?
| Modus | Erlaubt | Leert die Datei | Legt sie an | Zeiger startet |
r | nur lesen | nein | nein | am Anfang |
r+ | lesen und schreiben | nein | nein | am Anfang |
w | nur schreiben | ja, sofort | ja | am Anfang |
w+ | lesen und schreiben | ja, sofort | ja | am Anfang |
a | anhängen | nein | ja | am Ende |
a+ | lesen und anhängen | nein | ja | am Ende |
x | nur schreiben | nein | ja, sonst false | am Anfang |
c | nur schreiben | nein | ja | am Anfang |
Ein b lässt sich an jeden dieser Modi anhängen und schaltet in den Binärmodus. Warum das keine Kleinigkeit ist, klärt der vorletzte Abschnitt. Zwei Eigenheiten verdienen schon hier eine Erwähnung: Im Anhängemodus landet jeder Schreibvorgang am Dateiende, auch wenn der Zeiger vorher woanders stand. Und x ist der einzige Modus, der eine vorhandene Datei schützt, statt sie zu überschreiben.
Eine Datei mit PHP fopen() zeilenweise lesen
Die Leseschleife ist das Arbeitspferd. Sie holt immer nur eine Zeile in den Speicher, verarbeitet sie und vergisst sie wieder.
<?php
$fp = fopen('daten/lieferanten.csv', 'rb');
if ($fp === false) {
exit('Datei nicht lesbar.');
}
$nummer = 0;
while (!feof($fp)) {
$zeile = fgets($fp);
if ($zeile === false) {
break;
}
$nummer++;
echo $nummer . ': ' . rtrim($zeile, "\r\n") . PHP_EOL;
}
fclose($fp);
Zwei Details sind wichtig. feof() meldet das Dateiende erst, nachdem ein Lesevorgang darüber hinausgegangen ist, deshalb steht die zusätzliche Prüfung auf false im Rumpf. Und rtrim() entfernt den Zeilenumbruch, den fgets() mitliefert. Wer sich für die Feinheiten des zeilenweisen Lesens interessiert, findet sie in Dateien zeilenweise lesen mit fgets().
Speicherverbrauch: PHP fopen() gegen file()
Der Unterschied lässt sich messen, und die Zahlen sind deutlich. Beide Varianten verarbeiten dieselbe Datei von rund 190 MB.
<?php
/* Variante 1: alles auf einmal */
$zeilen = file('daten/gross.csv');
echo count($zeilen) . ' Zeilen, Spitze: ';
echo round(memory_get_peak_usage(true) / 1048576) . ' MB';
/* rund 190 Zeilen-Array plus Overhead, Spitze deutlich ueber 200 MB */
/* Variante 2: Zeile fuer Zeile */
$fp = fopen('daten/gross.csv', 'rb');
$anzahl = 0;
while (($zeile = fgets($fp)) !== false) {
$anzahl++;
}
fclose($fp);
echo $anzahl . ' Zeilen, Spitze: ';
echo round(memory_get_peak_usage(true) / 1048576) . ' MB';
/* Spitze bleibt im einstelligen MB-Bereich */
Die erste Variante hält jede Zeile als eigenen Eintrag im Arbeitsspeicher, die zweite immer nur eine. Deshalb wächst der Bedarf bei der Schleife nicht mit der Dateigröße. Das ist der eigentliche Grund, warum PHP fopen bei großen Dateien alternativlos ist.
Wenn PHP fopen() fehlschlägt
Die Meldung failed to open stream gehört zu den häufigsten überhaupt. Sie hat in aller Regel eine von drei Ursachen.
Der Pfad stimmt nicht Relative Pfade beziehen sich auf das Arbeitsverzeichnis des Prozesses, nicht auf den Ordner der Skriptdatei. getcwd() zeigt, wo PHP gerade steht, __DIR__ liefert einen verlässlichen Ausgangspunkt.
Die Rechte fehlen is_readable() und is_writable() beantworten das vor dem Öffnen. Beim Webserver zählen die Rechte des Serverbenutzers, nicht die des eigenen Kontos.
Was in dieser Lage nicht hilft, ist das Unterdrücken der Warnung mit einem vorangestellten @. Der Rückgabewert bleibt false, der Fehler tritt eine Zeile später auf, und die Meldung, die den Weg gewiesen hätte, ist weg. Sinnvoll ist das Gegenteil: prüfen, abbrechen und eine verständliche eigene Meldung ausgeben.
Mit PHP fopen() schreiben und anhängen
Der Unterschied zwischen w und a entscheidet darüber, ob eine Datei wächst oder jeden Tag neu beginnt.
<?php
/* Anhaengen: der Bestand bleibt stehen */
$fp = fopen('logs/zugriffe.log', 'a');
fwrite($fp, date('Y-m-d H:i:s') . " Aufruf\n");
fclose($fp);
/* Zum Vergleich: w leert die Datei beim Oeffnen,
noch bevor ueberhaupt etwas geschrieben wurde. */
$fp = fopen('logs/zugriffe.log', 'w');
/* Die Datei ist an dieser Stelle bereits leer. */
fclose($fp);
Der zweite Block ist der klassische Datenverlust. Das Leeren passiert beim Öffnen, nicht beim Schreiben. Selbst wenn das Skript danach abbricht, ist der alte Inhalt fort. Wer eine vorhandene Datei ausdrücklich nicht überschreiben will, nimmt x: Existiert sie schon, liefert der Aufruf false, ohne etwas anzurühren.
Paralleles Schreiben absichern
Sobald zwei Prozesse dieselbe Datei beschreiben, reicht der richtige Modus allein nicht mehr. Es braucht eine Sperre, und die Reihenfolge ist entscheidend.
<?php
/* c oeffnet zum Schreiben, ohne zu leeren.
Damit steht die Sperre, bevor Daten verloren gehen koennen. */
$fp = fopen('daten/zaehler.txt', 'c');
if ($fp === false) {
exit('Kein Zugriff.');
}
if (flock($fp, LOCK_EX)) {
$wert = (int) stream_get_contents($fp);
$wert++;
ftruncate($fp, 0);
rewind($fp);
fwrite($fp, (string) $wert);
fflush($fp);
flock($fp, LOCK_UN);
}
fclose($fp);
Mit w wäre dieser Ablauf kaputt, denn die Datei wäre bereits leer, bevor flock() überhaupt zum Zuge kommt. Ein zweiter Prozess würde dann eine halb geschriebene Datei vorfinden. Die Feinheiten der Sperren behandelt PHP flock() für sicheres paralleles Schreiben.
Mehr als Dateien: Wrapper und der Binärmodus
Der erste Parameter muss kein Dateipfad sein. PHP fopen versteht auch Adressen wie http://, ftp://, php://memory oder php://temp. Besonders die letzten beiden sind nützlich, weil sie sich wie eine Datei verhalten, ohne dass eine angelegt wird.
<?php
$fp = fopen('php://temp', 'r+');
fwrite($fp, "Erste Zeile\nZweite Zeile\n");
rewind($fp);
echo stream_get_contents($fp);
/* Erste Zeile
Zweite Zeile */
fclose($fp);
Das rewind() ist Pflicht, denn nach dem Schreiben steht der Zeiger am Ende und es gäbe nichts mehr zu lesen. Bei den Netzwerk-Wrappern gilt eine Einschränkung: Ist allow_url_fopen in der Serverkonfiguration abgeschaltet, funktioniert der Zugriff auf HTTP-Adressen nicht. Für Anfragen über das Netz ist cURL ohnehin die bessere Wahl, weil sich dort Zeitlimits, Kopfzeilen und Statuscodes auswerten lassen.
Das angehängte b schaltet in den Binärmodus. Unter Windows wandelt der Textmodus einen Zeilenumbruch beim Schreiben in zwei Zeichen um, was bei Bildern, ZIP-Archiven oder PDF-Dateien den Inhalt zerstört. Unter Linux ist das Flag wirkungslos. Daraus folgt eine einfache Regel: b immer setzen, es schadet nie.
Warum fclose() dazugehört
PHP gibt offene Handles am Skriptende automatisch frei. Das stimmt, taugt aber nur für kurze Anfragen als Begründung. Ein Worker-Prozess, der stundenlang läuft und in jedem Durchgang eine Datei öffnet, sammelt Handles an, bis das Systemlimit erreicht ist. Dann schlägt jedes weitere PHP fopen fehl, und zwar an einer Stelle, die mit der Ursache nichts zu tun hat.
Dazu kommt der Schreibpuffer. Erst fclose() oder fflush() stellt sicher, dass die Daten tatsächlich auf dem Datenträger stehen und nicht nur im Zwischenspeicher.
flowchart TD
A[fopen mit Modus] --> B{Handle oder false}
B -->|false| C[Ursache pruefen]
B -->|Handle| D{Lesen oder Schreiben}
D -->|Lesen| E[fgets in Schleife]
D -->|Schreiben| F[flock dann fwrite]
E --> G[fclose]
F --> G[fclose]
Das Diagramm fasst den Ablauf zusammen: erst prüfen, dann arbeiten, am Ende schließen. Jeder Zweig endet beim selben Aufruf.
Fazit
PHP fopen ist der Einstieg in jede Dateiarbeit, die mehr verlangt als ein einmaliges Lesen oder Schreiben. Der Handle erlaubt schrittweises Arbeiten, und genau das hält den Speicherbedarf unabhängig von der Dateigröße. Wer den Modus bewusst wählt statt aus Gewohnheit, vermeidet den häufigsten Datenverlust überhaupt. Und wer bei einer Fehlermeldung zuerst Pfad, Rechte und Verzeichnis prüft, statt die Warnung zu unterdrücken, findet die Ursache meist in einer Minute.