Eine Statusseite soll anzeigen, welches Release gerade ausgeliefert wird. Die Antwort liegt bereits im Dateisystem, denn der Webserver folgt einem symbolischen Link namens current, im englischen Handbuch symbolic link genannt. Genau dessen Inhalt liest PHP readlink() aus. Dabei ist der Unterschied zu realpath() von Anfang an wichtig: realpath() löst den gesamten Pfad bis zur echten Datei auf und liefert immer eine absolute Angabe, PHP readlink() gibt genau den einen Eintrag wieder, der in diesem Link steht, und zwar unverändert. Die übrigen Bausteine rund um Dateien und Pfade stehen im Artikel Dateihandling mit PHP.
Ein Link auf ein vorhandenes Releaseverzeichnis und ein Link ins Leere sehen im Verzeichnis völlig gleich aus, und daher rührt die halbe Verwirrung rund um Symlinks. Das Bild stellt beide Fälle nebeneinander.
Was PHP readlink() zurückgibt: string oder false
Der Rückgabewert ist eine Zeichenkette, und zwar genau die, die beim Anlegen des Links als Ziel angegeben wurde. Kein Pfad, der irgendwie berechnet oder ergänzt worden wäre, sondern der rohe Inhalt eines Verzeichniseintrags.
<?php
echo readlink('/var/www/current');
/* releases/2026-09-20 */
/* Zum Vergleich die Ausgabe der Shell:
lrwxrwxrwx 1 deploy deploy 19 Sep 20 03:10
/var/www/current -> releases/2026-09-20 */
Die Shell zeigt hinter dem Pfeil dasselbe Bruchstück, das PHP readlink() liefert. Wer den Wert ungeprüft als Dateipfad weiterreicht, arbeitet also mit einer Angabe, die es so im Dateisystem gar nicht gibt. Sie gilt nur relativ zu dem Verzeichnis, in dem der Link liegt.
Syntax: die Signatur im Original
readlink(string $path): string|false
/* path - the symbolic link path
return value - the contents of the symbolic link
on success, or false on failure
The function returns the target of a symbolic link.
It does not resolve the path any further, and it
emits a warning of level E_WARNING when the given
path is not a symbolic link. */
Zwei Angaben daraus tragen den Rest des Tutorials. Der Begriff contents ist bewusst gewählt, es geht um den Inhalt des Eintrags und nicht um ein aufgelöstes Ziel. Und ein false kommt nicht lautlos: PHP readlink() meldet zusätzlich eine Warnung, wenn der Pfad gar kein symbolischer Link ist.
PHP readlink() oder realpath(): der entscheidende Unterschied
Beide Funktionen bekommen denselben Pfad und antworten verschieden. Das ist kein Randfall: Ein so ausgelesener Pfad lässt sich später nicht öffnen, und der Fehler taucht erst dort auf, weit weg von seiner Ursache.
<?php
$pfad = '/var/www/current';
echo readlink($pfad) . "\n";
/* releases/2026-09-20 */
echo realpath($pfad) . "\n";
/* /var/www/releases/2026-09-20 */
flowchart TD
A[Pfad] --> B{Ist es ein Link?}
B -->|nein| C[readlink gibt false]
B -->|ja| D[readlink liest den Eintrag]
D --> E{Ziel absolut?}
E -->|ja| F[Pfad ist fertig]
E -->|nein| G[dirname plus Ziel]
A --> H[realpath loest alles auf]
H --> F
| Frage | readlink() | realpath() |
| Was kommt an? | der Inhalt des Linkeintrags | der vollständig aufgelöste Pfad |
| Immer absolut? | nur wenn der Link selbst absolut angelegt wurde | ja, ausnahmslos |
| Kette aus mehreren Links | geht genau einen Schritt | folgt bis zum Ende |
| Ziel fehlt | liefert den Eintrag trotzdem | liefert false |
| Pfad ist kein Link | false und eine Warnung | der normalisierte Pfad |
Daraus folgt eine einfache Faustregel. Wer wissen will, wohin ein Link zeigt, nimmt PHP readlink(). Wer eine Datei öffnen oder vergleichen will, nimmt realpath().
Wer den Namen vom Terminal kennt, sollte dabei einen Unterschied beachten. Das Kommandozeilenwerkzeug readlink gibt ohne Optionen dasselbe aus wie die PHP-Funktion, nämlich den rohen Eintrag. Mit der Option -f löst es dagegen die ganze Kette auf und verhält sich damit wie realpath(). PHP readlink() entspricht also dem Aufruf ohne Optionen. Wer ein Shell-Skript nachbaut, in dem readlink -f steht, braucht in PHP die andere Funktion.
Nicht für Pfadprüfungen verwenden Soll sichergestellt werden, dass ein hochgeladener Pfad innerhalb eines erlaubten Verzeichnisses bleibt, ist PHP readlink() das falsche Werkzeug. Es geht genau einen Schritt, und hinter dem Ergebnis kann der nächste Link warten. Richtig ist die vollständige Auflösung mit realpath() und erst danach der Vergleich mit dem erlaubten Verzeichnis.
is_link() als Vorprüfung, und warum false hier normal ist
Ein Aufruf auf eine gewöhnliche Datei liefert false und schreibt eine Warnung ins Fehlerprotokoll. Bei einer Schleife über ein großes Verzeichnis füllt sich die Datei damit in Sekunden. Eine Zeile is_link() davor, und das Protokoll bleibt lesbar.
<?php
$pfad = '/var/www/current';
if (is_link($pfad)) {
echo readlink($pfad);
} else {
echo 'Kein symbolischer Link';
}
Wichtig ist die Einordnung des Ergebnisses. Ein false bedeutet hier nicht, dass etwas schiefgegangen wäre. Es ist die zutreffende Antwort auf einen Pfad, der einfach kein Link ist. Wer das als Fehlerfall behandelt und eine Ausnahme wirft, baut sich Alarme, die nichts melden. Dieselbe Überlegung gilt bei den verwandten Prüfungen, etwa bei is_readable() und seinen Antworten.
Wo die Vorprüfung nicht reicht, etwa bei einem Verzeichnis, das ein anderer Prozess gerade aufräumt, ist das Stummschalten mit @readlink($pfad) und die Prüfung auf false die ehrlichere Lösung als ein try/catch, das nie auslöst: PHP readlink() wirft keine Ausnahme, es warnt nur.
Der tote Link: warum file_exists() das Falsche meldet
Zeigt ein Link auf ein gelöschtes Verzeichnis, entsteht eine Lage, die viele zum ersten Mal ratlos macht. Der Eintrag ist im Verzeichnis sichtbar, aber die gewohnte Existenzprüfung verneint ihn.
<?php
$link = '/var/www/current';
var_dump(is_link($link)); /* bool(true) */
var_dump(file_exists($link)); /* bool(false) */
var_dump(readlink($link));
/* string(19) "releases/2026-09-20" */
Kaputt ist daran nichts. file_exists(), is_file() und is_dir() folgen dem Link und bewerten dessen Ziel, und das Ziel gibt es nicht mehr. is_link() bewertet dagegen den Eintrag selbst. Aus der Kombination beider Antworten entsteht die Erkennung verwaister Links.
<?php
function linkstatus(string $pfad): string
{
if (!is_link($pfad)) {
return 'kein Link';
}
$ziel = readlink($pfad);
if ($ziel === false) {
return 'Linkziel nicht lesbar';
}
$voll = $ziel;
if ($ziel[0] !== '/') {
$voll = dirname($pfad) . '/' . $ziel;
}
return file_exists($voll)
? "zeigt auf $ziel"
: "verwaist, Ziel $ziel fehlt";
}
Diese Funktion beantwortet in einem Durchgang die drei Fragen, die bei einem Verzeichniseintrag überhaupt offen sind: kein Link, gesunder Link, verwaister Link. Sie gehört in jedes Werkzeug, das ein Verzeichnis durchläuft und dabei auf Symlinks trifft.
Relative Linkziele richtig zusammensetzen
Ein relatives Ziel gilt immer relativ zu dem Verzeichnis, in dem der Link liegt. Nicht relativ zum Arbeitsverzeichnis des Skripts, und das ist der Punkt, an dem die meisten Fehler entstehen. Ein Cronjob startet in einem anderen Verzeichnis als der Webserver, und derselbe Code liefert plötzlich andere Ergebnisse.
<?php
$link = '/var/www/current';
$ziel = readlink($link);
if ($ziel !== false && $ziel[0] !== '/') {
$ziel = dirname($link) . '/' . $ziel;
}
echo realpath($ziel);
/* /var/www/releases/2026-09-20 */
Der Bauplan lautet also: dirname() des Links, Schrägstrich, dann das Ergebnis von PHP readlink(). Was dabei herauskommt, enthält oft noch Bestandteile wie .., und genau dafür ist realpath() als zweiter Schritt gedacht. Die beiden Funktionen konkurrieren nicht, sie arbeiten hier zusammen.
Praxis: den Releaselink eines Deployments auslesen
Ein verbreiteter Aufbau legt jedes Release in ein eigenes datiertes Verzeichnis unter releases/ und zeigt mit dem Link current auf die aktive Fassung. Umschalten heißt dann, den Link neu zu setzen. Wie solche Abläufe aufgebaut werden, steht in PHP Deployment-Strategien von FTP bis CI/CD. Hier geht es nur um die Rückfrage: Welche Fassung läuft gerade?
<?php
function aktivesRelease(string $link): ?string
{
if (!is_link($link)) {
return null;
}
$ziel = readlink($link);
return $ziel === false ? null : basename($ziel);
}
$release = aktivesRelease('/var/www/current');
echo $release ?? 'unbekannt';
/* 2026-09-20 */
Der Gewinn gegenüber einer zusätzlichen Versionsdatei liegt darin, dass es nur eine Wahrheit gibt. Eine Datei, die beim Deployment mitgeschrieben wird, kann veralten, wenn ein Schritt abbricht. Der Link dagegen ist genau das, was der Webserver ausliefert. PHP readlink() fragt damit die Quelle selbst und nicht deren Abschrift.
Für die Statusseite reicht anschließend basename(), weil der Releasename im letzten Pfadteil steht. Wer den vollen Pfad braucht, etwa für einen Link auf das Protokoll des Laufs, setzt ihn wie im vorigen Abschnitt zusammen.
Ein Nachsatz für langlebige Prozesse. PHP merkt sich aufgelöste Pfade im Realpath-Cache, und OPcache hängt daran. Wird current auf ein neues Release umgelegt, liefert realpath() im laufenden FPM-Arbeiter deshalb bis zu realpath_cache_ttl Sekunden lang noch den alten Pfad, und OPcache gibt weiter die Dateien des alten Release aus. PHP readlink() liest dagegen bei jedem Aufruf den Verzeichniseintrag im Filesystem. Für eine Statusseite, die sagen soll, was gerade ausgeliefert wird, ist das ein weiteres Argument für diese Funktion. Zum Umschalten selbst gehört dann opcache_reset() oder ein Neustart des Pools.
lstat() gegen stat(): den Link selbst betrachten
stat() folgt dem Link und beschreibt das Ziel, lstat() beschreibt den Link. Am deutlichsten wird das an der Größe: Ein Linkeintrag ist so groß wie die Zeichenkette, die er enthält.
<?php
$link = '/var/www/current';
$l = lstat($link);
$s = stat($link);
echo $l['size'] . " Byte im Linkeintrag\n";
echo $s['size'] . " Byte im Zielverzeichnis";
/* 19 Byte im Linkeintrag
4096 Byte im Zielverzeichnis */
Neunzehn Byte, weil releases/2026-09-20 neunzehn Zeichen lang ist. filesize() verhält sich wie stat() und folgt dem Link ebenfalls. Genau deshalb zählt eine Größenberechnung über ein Verzeichnis verlinkte Dateien doppelt, solange sie diese nicht vorher mit is_link() aussortiert.
symlink(), linkinfo() und das Verhalten unter Windows
Angelegt wird ein Link mit symlink($ziel, $link). Damit schließt sich der Kreis, denn was dort hineingeschrieben wird, kommt bei PHP readlink() wieder heraus.
<?php
symlink('/var/www/releases/2026-09-20',
'/var/www/current');
echo readlink('/var/www/current');
/* /var/www/releases/2026-09-20 */
/* Harte Links kennen kein Ziel zum Auslesen:
link() zeigt auf denselben Inode, und
readlink() meldet dort false. */
Ein harter Link ist ein zweiter Name für dieselben Daten, kein Verweis auf einen Pfad. Es gibt dort nichts auszulesen. linkinfo() wiederum liefert eine Kennung des Geräts und beantwortet damit eine andere Frage. Für die schlichte Feststellung, ob ein Eintrag ein Link ist, bleibt is_link() der richtige Weg.
Unter Windows gibt es symbolische Links ebenfalls, und PHP readlink() liest sie aus. Ihr Anlegen setzt allerdings erhöhte Rechte oder den eingeschalteten Entwicklermodus voraus. Junctions sind eine eigene Bauform für Verzeichnisse und verhalten sich nicht in jedem Punkt wie ein Symlink. Wer plattformübergreifend entwickelt, sollte diesen Teil auf der Zielplattform ausprobieren statt sich auf eine allgemeine Zusage zu verlassen.
Fazit
PHP readlink() liefert den Inhalt eines Verzeichniseintrags, nicht einen fertigen Pfad. Diese eine Unterscheidung räumt die meisten Missverständnisse aus. Ist das Ziel relativ, gehört dirname() des Links davor und bei Bedarf realpath() dahinter. Wer den ganzen Weg bis zur echten Datei will, nimmt gleich realpath().
Praktisch sind zwei Dinge dauerhaft nützlich. Die Vorprüfung mit is_link() hält Warnungen aus dem Protokoll und macht zugleich verwaiste Links sichtbar, weil file_exists() dort widerspricht. Und der Releaselink eines Deployments beantwortet die Frage nach der laufenden Fassung ohne zweite Datenquelle, mit einem Aufruf von PHP readlink() und einem basename() dahinter.