Eine Rechnung liegt außerhalb des Webverzeichnisses und soll nur nach dem Login erreichbar sein. Ein direkter Link scheidet damit aus, denn er würde die Prüfung umgehen. Gebraucht wird ein Skript, das die Datei ausliefert, nachdem es die Berechtigung geprüft hat.
Das Bild zeigt den kurzen Weg: von der Datei direkt in die Antwort, ohne Zwischenstation im Arbeitsspeicher. Genau diesen Weg beschreibt der Text.
Was PHP readfile() macht
Die Signatur lautet readfile(string $filename, bool $use_include_path = false, $context = null): int|false. Der Rückgabewert wird oft übersehen und ist trotzdem nützlich: Es ist die Anzahl der ausgegebenen Bytes, und bei einem Fehlschlag steht dort false.
<?php
$bytes = readfile('daten/hinweise.txt');
if ($bytes === false) {
exit('Datei konnte nicht ausgegeben werden.');
}
/* $bytes enthaelt jetzt die tatsaechlich gelieferte Menge,
etwa 2048 bei einer 2 KB grossen Datei. */
Diese Zahl eignet sich als Protokollmerkmal. Wer sie mitschreibt, erkennt abgebrochene Übertragungen daran, dass die gelieferte Menge kleiner ist als die Dateigröße. Ein bloßer Eintrag "Download gestartet" liefert diese Auskunft nicht.
Die Signatur im Original
Die Handbuchseite ist knapp gehalten, und ihre englischen Parameternamen tauchen in jeder Fehlermeldung wieder auf. Deshalb lohnt der Blick auf die Beschreibung im Original, bevor es an die Praxis geht.
readfile(string $filename, bool $use_include_path = false, $context = null): int|false
/* filename - the file to read, absolute or relative path
use_include_path - search for a file in the include_path as well
context - a stream context resource, see below
return value - number of bytes read from the file and written
to the output buffer, or false on failure.
Equals the size of the file when nothing was cut off. */
Im Deutschen liest sich das so: Der erste Parameter ist der Pfad, der zweite erweitert die Suche auf den Include-Pfad, der dritte nimmt einen Stream-Kontext. Zurück kommt die Zahl der ausgegebenen Bytes.
Die beiden anderen Parameter
Der erste Parameter ist der Dateiname, und dabei bleibt es in fast allen Beispielen. Die Funktion nimmt aber zwei weitere entgegen, und beide sind in bestimmten Lagen nützlich.
<?php
/* Zweiter Parameter: die Datei auch im include_path suchen.
Praktisch, wenn Vorlagen in einem Verzeichnis liegen,
das ohnehin schon im Suchpfad steht. */
readfile('vorlagen/kopf.html', true);
/* Dritter Parameter: ein Stream-Kontext.
Damit lassen sich Zeitlimits und Kopfzeilen setzen,
wenn die Quelle nicht auf der eigenen Platte liegt. */
$kontext = stream_context_create([
'http' => [
'timeout' => 5,
'header' => "User-Agent: php-space-bot\r\n",
],
]);
readfile('https://beispiel.tld/liste.txt', false, $kontext);
Der include_path ist derselbe, den auch include und require benutzen. Er steht in der php.ini und lässt sich zur Laufzeit mit get_include_path() auslesen. Für Dateien mit absolutem Pfad hat der Parameter keine Wirkung.
Beim dritten Parameter ist eine Einschränkung wichtig: Der Zugriff auf eine Adresse mit http:// funktioniert nur, wenn allow_url_fopen in der php.ini eingeschaltet ist. Viele Hoster schalten es ab. Für Anfragen über das Netz ist cURL ohnehin die bessere Wahl, weil sich dort der Statuscode auswerten lässt, was hier nicht geht: PHP readfile liefert auch eine Fehlerseite klaglos aus.
PHP readfile() gibt aus, file_get_contents() gibt zurück
Diese Unterscheidung erklärt fast alle Missverständnisse rund um die Funktion. file_get_contents() holt den Inhalt in eine Variable, damit das Skript damit arbeiten kann. PHP readfile schreibt ihn dagegen unmittelbar in den Ausgabestrom, ohne dass er je in einer Variablen landet.
Wer eine Datei durchreichen will, braucht die Variable also gar nicht. Wer den Inhalt vorher verändern möchte, kommt an ihr nicht vorbei. Das Einlesen in eigene Variablen zeigt Dateien lesen und ausgeben in PHP ausführlich, hier geht es um die Gegenrichtung.
Eine Datei zum Download anbieten
Das folgende Skript ist der praktische Kern des Tutorials. Es setzt vier Kopfzeilen, gibt die Datei aus und beendet sich sofort danach.
<?php
$pfad = '/var/kundendaten/rechnung-2026-08.pdf';
$name = 'Rechnung August 2026.pdf';
if (!is_readable($pfad)) {
http_response_code(404);
exit('Nicht gefunden.');
}
$typ = (new finfo(FILEINFO_MIME_TYPE))->file($pfad);
header('Content-Type: ' . $typ);
header('Content-Length: ' . filesize($pfad));
header('Content-Disposition: attachment; filename="rechnung.pdf"; '
. "filename*=UTF-8''" . rawurlencode($name));
header('Cache-Control: private, no-store');
while (ob_get_level() > 0) {
ob_end_clean();
}
readfile($pfad);
exit;
Das abschließende exit ist keine Zierde. Jedes Byte, das das Skript danach noch ausgibt, landet in der ausgelieferten Datei und macht sie unbrauchbar. Schon ein Leerzeichen hinter einem schließenden PHP-Tag reicht, damit ein PDF nicht mehr öffnet.
Die vier Kopfzeilen und was sie bewirken
Content-Type Sagt dem Browser, worum es sich handelt. Fehlt die Angabe, rät er anhand des Inhalts, und ein PDF landet als Zeichensalat im Fenster. Der Typ kommt aus finfo, nicht aus der Dateiendung, denn die lässt sich beliebig setzen.
Content-Disposition Mit attachment lädt der Browser herunter, statt anzuzeigen. Der Zusatz filename*=UTF-8'' trägt den Namen mit Umlauten, das einfache filename= davor dient als Rückfallebene für ältere Programme.
Content-Length Erst diese Angabe erlaubt eine sinnvolle Fortschrittsanzeige. Sie sollte nur bei lokalen Dateien gesetzt werden, weil filesize() nicht über jeden Wrapper zuverlässig arbeitet.
Cache-Control Verhindert, dass ein Zwischenspeicher eine persönliche Datei aufbewahrt und später an jemand anderen ausliefert.
Wer tiefer in das Zusammenspiel der Kopfzeilen einsteigen will, findet die Grundlagen in HTTP-Header mit PHP. Für die Auslieferung reichen die vier oben.
Braucht PHP readfile() viel Speicher?
Zu dieser Frage stehen im Netz zwei gegensätzliche Behauptungen, und beide sind unvollständig. Die Funktion selbst liest blockweise und hält die Datei nicht am Stück im Arbeitsspeicher. Der Ausgabepuffer von PHP tut das allerdings sehr wohl, sobald er aktiv ist.
<?php
/* Ohne diese Schleife sammelt ein aktiver Ausgabepuffer
die komplette Datei im Arbeitsspeicher an. */
while (ob_get_level() > 0) {
ob_end_clean();
}
readfile('/var/archiv/video-4gb.mp4');
exit;
Damit löst sich der Widerspruch auf. PHP readfile ist auch bei einer 4 GB großen Datei unproblematisch, solange kein Puffer dazwischensteht. Viele Anwendungen schalten die Pufferung global ein, deshalb gehört die Schleife in jedes Auslieferungsskript.
Dateien sicher ausliefern
Der häufigste Fehler in solchen Skripten ist der Parameter, der direkt zum Pfad wird. Er lässt sich in einer Zeile ausnutzen.
<?php
/* So nicht: der Aufruf ?datei=../../config.php
liefert die Konfiguration inklusive Zugangsdaten aus. */
readfile('downloads/' . $_GET['datei']);
Die Absicherung besteht aus drei Schritten: eine feste Basis vorgeben, den Parameter auf den reinen Dateinamen reduzieren und das Ergebnis gegen die Basis prüfen.
<?php
$basis = realpath('/var/downloads');
$name = basename((string) ($_GET['datei'] ?? ''));
$pfad = realpath($basis . '/' . $name);
if ($pfad === false || !str_starts_with($pfad, $basis . DIRECTORY_SEPARATOR)) {
http_response_code(403);
exit('Nicht erlaubt.');
}
header('Content-Type: application/octet-stream');
readfile($pfad);
exit;
basename() entfernt jeden Verzeichnisanteil, und realpath() löst Verweise wie .. auf, bevor verglichen wird. Erst beide Schritte zusammen schließen den Angriff aus. Der Vergleich mit angehängtem Trennzeichen verhindert außerdem, dass ein Nachbarverzeichnis mit ähnlichem Namen durchrutscht.
readfile(), fpassthru() und include im Vergleich
| Funktion | Ergebnis | Führt Code aus | Typischer Einsatz |
readfile() | gibt direkt aus | nein | Downloads, geschützte Dateien |
file_get_contents() | gibt zurück | nein | Inhalt weiterverarbeiten |
fpassthru() | gibt direkt aus | nein | Handle ist schon offen |
include | gibt aus | ja | eigene Vorlagen |
Die letzte Zeile enthält den wichtigsten Unterschied. Wird eine hochgeladene HTML-Datei per include eingebunden und enthält sie einen PHP-Block, führt der Server ihn aus. Fremde Inhalte gehören deshalb nie in ein include, sondern in eine Ausgabe ohne Auswertung.
fpassthru() leistet dasselbe wie PHP readfile, setzt aber einen bereits geöffneten Handle voraus. Das ist praktisch, wenn vorher schon Teile der Datei gelesen wurden, etwa um eine Signatur zu prüfen. Wie ein solcher Handle entsteht, zeigt PHP fopen() zum Öffnen, Lesen und Schreiben von Dateien.
Wenn PHP nicht mehr der richtige Ort ist
Jeder laufende Download bindet einen PHP-Prozess, und zwar so lange, bis das letzte Byte beim Empfänger ist. Bei zweihundert gleichzeitigen Downloads ist der Prozesspool erschöpft, obwohl der Server kaum rechnet.
Dafür gibt es eine Abhilfe auf Webserver-Ebene. Das Skript prüft die Berechtigung, setzt eine Kopfzeile mit dem Pfad und beendet sich. Der Webserver übernimmt die eigentliche Auslieferung. Bei Apache heißt die Kopfzeile X-Sendfile, bei nginx X-Accel-Redirect. Der Umstieg lohnt sich, sobald große Dateien häufig parallel abgerufen werden. Für gelegentliche Downloads bleibt PHP readfile die einfachere und völlig ausreichende Lösung.
flowchart TD
A[Anfrage mit Datei-ID] --> B{Zugriff erlaubt}
B -->|nein| C[403 und Abbruch]
B -->|ja| D[Pfad aus fester Basis bauen]
D --> E[Header setzen]
E --> F[Ausgabepuffer leeren]
F --> G[readfile und exit]
Der Ablauf im Diagramm ist bewusst starr. Jede Abkürzung an einer der Stationen führt zu einem der Fehler, die oben beschrieben sind.
Fazit
PHP readfile ist der kurze Weg, eine Datei unverändert an den Browser zu geben. Die Funktion selbst ist unauffällig, entscheidend ist ihr Umfeld: die richtigen Kopfzeilen, ein geleerter Ausgabepuffer und ein Pfad, der niemals aus einem Parameter stammt. Wer diese drei Punkte beachtet, liefert auch mehrere Gigabyte große Dateien zuverlässig aus. Und wer merkt, dass die Downloads den Prozesspool auffressen, weiß jetzt, an welcher Stelle der Webserver die Arbeit übernimmt.