readlink
Returns the target path stored in a symbolic link, without following it any further.
readlink(string $path): string|false- Added in PHP 4.0
Parameters
| Parameter | Type | Description |
|---|---|---|
$path | string | Path of the symbolic link to read. |
Return value
string|falseThe contents of the symbolic link as a string, or false on error.
Errors
Emits an E_WARNING and returns false when the path is not a symbolic link or cannot be read. On Windows a non-link path returns its normalised form instead of failing.
readlink() returns exactly the string that was stored when the symbolic link was created — one hop, no resolution. If the link was made with a relative target such as ../releases/v3, that is literally what comes back, and it is relative to the link's own directory, not to the current working directory.
When to reach for it
The classic case is a deployment layout where current is a symlink into a releases/ directory: readlink('current') tells you which release is live, which is exactly what a status or rollback script wants to print. It is also how you audit a directory tree for links that point somewhere unexpected, and how you detect a dangling link — one whose target no longer exists.
What trips people up
Two behaviours cause most confusion. First, on a path that is not a symlink, readlink() warns and returns false on Unix, but on Windows it returns the normalised path instead — so test with is_link() first if your code runs on both. Second, readlink() is not realpath(): for a chain of links it gives you the next hop only, and it happily returns a target that does not exist, whereas realpath() follows the chain to the end and returns false if anything is missing.
Example
<?php
mkdir('releases/v3', 0775, true);
file_put_contents('releases/v3/VERSION', "3.0.0\n");
symlink('releases/v3', 'current');
var_dump(is_link('current')); // true
echo readlink('current'), "\n"; // releases/v3 - the stored target
echo realpath('current'), "\n"; // fully resolved absolute path
unlink('current');
unlink('releases/v3/VERSION');
rmdir('releases/v3'); rmdir('releases');When to use it
- Print which release directory the 'current' symlink points at in a deploy status command.
- Detect dangling symlinks in a media directory during a housekeeping job.
- Audit a tree for links whose target escapes the intended base directory.
Frequently asked questions
What is the difference between readlink and realpath?
readlink() returns the raw target stored in one symlink and does not verify it exists; realpath() follows the entire chain of links and .. segments and returns an absolute path, or false if any part is missing.
Why does readlink return false?
Usually because the path is not a symbolic link — check with is_link() first — or because the process lacks execute permission on a parent directory. On Windows, a non-link path returns the normalised path instead of false.
Does readlink follow a chain of symlinks?
No, it returns only the next hop. To reach the final destination, call it in a loop guarded by is_link(), or just use realpath().
More examples
Resolving a relative link target
The stored target is relative to /srv/app, not to the process's working directory, so it must be joined with dirname($link) before use. realpath() then confirms whether the target actually exists.
<?php
$link = '/srv/app/current';
if (!is_link($link)) {
exit("$link is not a symlink\n");
}
$target = readlink($link); // e.g. "../releases/2026-08-30"
$abs = str_starts_with($target, '/')
? $target
: dirname($link) . '/' . $target;
echo "stored: $target\n";
echo "resolved: ", realpath($abs) ?: '(missing)', "\n";Finding dangling links
readlink() succeeds even when the target is gone, so it alone cannot tell you a link is broken. Pairing it with realpath(), which returns false for a missing target, is what distinguishes a healthy link from a dangling one.
<?php
$dir = '/srv/media';
foreach (scandir($dir) as $entry) {
$path = "$dir/$entry";
if (!is_link($path)) { continue; }
$target = readlink($path);
$ok = realpath($path) !== false;
printf("%-20s -> %-30s %s\n", $entry, $target, $ok ? 'OK' : 'DANGLING');
}
Discussion