49

I noticed some similar questions about this problem when I typed the title, but they seem not be in PHP. So what's the solution to it with a PHP function?

To be specified.

$a="/home/apache/a/a.php";
$b="/home/root/b/b.php";
$relpath = getRelativePath($a,$b); //needed function,should return '../../root/b/b.php'

Any good ideas? Thanks.

Young
  • 7,986
  • 7
  • 43
  • 64
  • 11
    What's your use case for needing a relative path when you have the real path? – Tim Lytle Apr 14 '10 at 14:03
  • Could you please post those similar questions? Writing port for PHP is easier that reinviting everything. – Crozin Apr 14 '10 at 14:04
  • 2
    @Tim Lytle,I think it maybe some sense when do some link/include stuff.And also for interest. – Young Apr 14 '10 at 14:11
  • For those interested in a PowerShell port, I've written one: http://stackoverflow.com/questions/13239620/getting-relative-path-from-absolute-path-in-powershellt – ComFreek Nov 06 '12 at 22:07
  • 1
    Several answers have been proposed. I benchmarked them all, so please read my post below before deciding which one to use ;-) – lucaferrario Feb 18 '13 at 21:23
  • Useful for creating symlinks. – Bell Jun 04 '17 at 01:21

11 Answers11

72

Try this one:

function getRelativePath($from, $to)
{
    // some compatibility fixes for Windows paths
    $from = is_dir($from) ? rtrim($from, '\/') . '/' : $from;
    $to   = is_dir($to)   ? rtrim($to, '\/') . '/'   : $to;
    $from = str_replace('\\', '/', $from);
    $to   = str_replace('\\', '/', $to);

    $from     = explode('/', $from);
    $to       = explode('/', $to);
    $relPath  = $to;

    foreach($from as $depth => $dir) {
        // find first non-matching dir
        if($dir === $to[$depth]) {
            // ignore this directory
            array_shift($relPath);
        } else {
            // get number of remaining dirs to $from
            $remaining = count($from) - $depth;
            if($remaining > 1) {
                // add traversals up to first matching dir
                $padLength = (count($relPath) + $remaining - 1) * -1;
                $relPath = array_pad($relPath, $padLength, '..');
                break;
            } else {
                $relPath[0] = './' . $relPath[0];
            }
        }
    }
    return implode('/', $relPath);
}

This will give

$a="/home/a.php";
$b="/home/root/b/b.php";
echo getRelativePath($a,$b), PHP_EOL;  // ./root/b/b.php

and

$a="/home/apache/a/a.php";
$b="/home/root/b/b.php";
echo getRelativePath($a,$b), PHP_EOL; // ../../root/b/b.php

and

$a="/home/root/a/a.php";
$b="/home/apache/htdocs/b/en/b.php";
echo getRelativePath($a,$b), PHP_EOL; // ../../apache/htdocs/b/en/b.php

and

$a="/home/apache/htdocs/b/en/b.php";
$b="/home/root/a/a.php";
echo getRelativePath($a,$b), PHP_EOL; // ../../../../root/a/a.php
Gordon
  • 312,688
  • 75
  • 539
  • 559
  • 1
    Looks like we came up with almost the exact same algorithm, mine in english, yours in PHP (me being too lazy too code it, you not being). – webbiedave Apr 14 '10 at 14:58
  • 1
    I tested this and all the others functions and you can find my benchmarks results in my post scrolling down. This implementation came out to be the best, so I suggest using this one! ;-) – lucaferrario Feb 14 '13 at 15:28
  • DO NOT USE THIS FUNCTION, it returns a WRONG result in this case `getRelativePath("/home/some/../ws.a", "/home/modules/../ws.b");` => `../../modules/../ws.b` http://sandbox.onlinephpfunctions.com/code/2634034cb4b3688b319a97b5a45ea2442ac2d419 – Edwin Rodríguez Jun 12 '20 at 11:18
  • 1
    @EdwinRodríguez You are passing in relative paths. The OP asked about absolute paths as input only. Run them through `realpath` first to make them absolute. – Gordon Jun 15 '20 at 09:51
  • @Gordon I'm passing absolute paths because they include the root. Running `realpath` does not solve the problem if the files do not exist in the filesystem – Edwin Rodríguez Jun 16 '20 at 11:08
  • @EdwinRodríguez well, then don't use the function. It doesn't work in your specific scenario. – Gordon Jun 17 '20 at 06:28
20

Since we've had several answers, I decided to test them all and benchmark them. I used this paths to test:

$from = "/var/www/sites/web/mainroot/webapp/folder/sub/subf/subfo/subfol/subfold/lastfolder/"; NOTE: if it is a folder, you have to put a trailing slash for functions to work correctly! So, __DIR__ will not work. Use __FILE__ instead or __DIR__ . '/'

$to = "/var/www/sites/web/mainroot/webapp/folder/aaa/bbb/ccc/ddd";

RESULTS: (decimal separator is comma, thousand separator is dot)

  • Function by Gordon: result CORRECT, time for 100.000 execs 1,222 seconds
  • Function by Young: result CORRECT, time for 100.000 execs 1,540 seconds
  • Function by Ceagle: result WRONG (it works with some paths but fails with some others, like the ones used in the tests and written above)
  • Function by Loranger: result WRONG (it works with some paths but fails with some others, like the ones used in the tests and written above)

So, I suggest that you use Gordon's implementation! (the one marked as answer)

Young's one is good too and performs better with simple directory structures (like "a/b/c.php"), while Gordon's one performs better with complex structures, with lots of subdirectories (like the ones used in this benchmark).


NOTE: I write here below the results returned with $from and $to as inputs, so you can verify that 2 of them are OK, while other 2 are wrong:

  • Gordon: ../../../../../../aaa/bbb/ccc/ddd --> CORRECT
  • Young: ../../../../../../aaa/bbb/ccc/ddd --> CORRECT
  • Ceagle: ../../../../../../bbb/ccc/ddd --> WRONG
  • Loranger: ../../../../../aaa/bbb/ccc/ddd --> WRONG
lucaferrario
  • 990
  • 9
  • 9
  • Nice benchmark. You did test against folders instead of files (as requested), that's the reason why my function failed to return the correct path. Anyway, you're right, in order to be reliable this function should always return the correct path, regardless it uses files or folders, so I did simply fix this issue. I would be curious to get my results. Could you please benchmark another time ? – loranger Feb 28 '13 at 01:19
  • 1
    I'm sorry Loranger, but writing a reliable benchmarking script took me some time...and unfortunately I don't have it anymore and I don't have the time to write another one to repeat the test. Anyway, if you've fixed the issue, well done! :-) – lucaferrario Jan 08 '14 at 18:03
9

Relative path? This seems more like a travel path. You seem to want to know the path one travels to get from path A to path B. If that's the case, you can explode $a and $b on '/' then inversely loop through the $aParts, comparing them to $bParts of the same index until the "common denominator" directory is found (recording the number of loops along the way). Then create an empty string and add '../' to it $numLoops-1 times then add to that $b minus the common denominator directory.

webbiedave
  • 48,414
  • 8
  • 88
  • 101
6
const DS = DIRECTORY_SEPARATOR; // for convenience

function getRelativePath($from, $to) {
    $dir = explode(DS, is_file($from) ? dirname($from) : rtrim($from, DS));
    $file = explode(DS, $to);

    while ($dir && $file && ($dir[0] == $file[0])) {
        array_shift($dir);
        array_shift($file);
    }
    return str_repeat('..'.DS, count($dir)) . implode(DS, $file);
}

My attempt is deliberately simpler, although probably no different in performance. I'll leave benchmarking as an exercise for the curious reader. However, this is fairly robust and should be platform agnostic.

Beware the solutions using array_intersect functions as these will break if parallel directories have the same name. For example getRelativePath('start/A/end/', 'start/B/end/') would return "../end" because array_intersect finds all the equal names, in this case 2 when there should only be 1.

clockworkgeek
  • 37,650
  • 9
  • 89
  • 127
  • This is almost identical to the solution I just came up with, except a little bit cleaner with the `str_repeat` and `dirname`. Love it! Thanks. – mpen Jan 30 '14 at 03:41
3

This code is taken from Symfony URL generator https://github.com/symfony/Routing/blob/master/Generator/UrlGenerator.php

    /**
     * Returns the target path as relative reference from the base path.
     *
     * Only the URIs path component (no schema, host etc.) is relevant and must be given, starting with a slash.
     * Both paths must be absolute and not contain relative parts.
     * Relative URLs from one resource to another are useful when generating self-contained downloadable document archives.
     * Furthermore, they can be used to reduce the link size in documents.
     *
     * Example target paths, given a base path of "/a/b/c/d":
     * - "/a/b/c/d"     -> ""
     * - "/a/b/c/"      -> "./"
     * - "/a/b/"        -> "../"
     * - "/a/b/c/other" -> "other"
     * - "/a/x/y"       -> "../../x/y"
     *
     * @param string $basePath   The base path
     * @param string $targetPath The target path
     *
     * @return string The relative target path
     */
    function getRelativePath($basePath, $targetPath)
    {
        if ($basePath === $targetPath) {
            return '';
        }

        $sourceDirs = explode('/', isset($basePath[0]) && '/' === $basePath[0] ? substr($basePath, 1) : $basePath);
        $targetDirs = explode('/', isset($targetPath[0]) && '/' === $targetPath[0] ? substr($targetPath, 1) : $targetPath);
        array_pop($sourceDirs);
        $targetFile = array_pop($targetDirs);

        foreach ($sourceDirs as $i => $dir) {
            if (isset($targetDirs[$i]) && $dir === $targetDirs[$i]) {
                unset($sourceDirs[$i], $targetDirs[$i]);
            } else {
                break;
            }
        }

        $targetDirs[] = $targetFile;
        $path = str_repeat('../', count($sourceDirs)).implode('/', $targetDirs);

        // A reference to the same base directory or an empty subdirectory must be prefixed with "./".
        // This also applies to a segment with a colon character (e.g., "file:colon") that cannot be used
        // as the first segment of a relative-path reference, as it would be mistaken for a scheme name
        // (see http://tools.ietf.org/html/rfc3986#section-4.2).
        return '' === $path || '/' === $path[0]
            || false !== ($colonPos = strpos($path, ':')) && ($colonPos < ($slashPos = strpos($path, '/')) || false === $slashPos)
            ? "./$path" : $path;
    }
ya.teck
  • 2,060
  • 28
  • 34
2

Based on Gordon's function,my solution is as follows:

function getRelativePath($from, $to)
{
   $from = explode('/', $from);
   $to = explode('/', $to);
   foreach($from as $depth => $dir)
   {

        if(isset($to[$depth]))
        {
            if($dir === $to[$depth])
            {
               unset($to[$depth]);
               unset($from[$depth]);
            }
            else
            {
               break;
            }
        }
    }
    //$rawresult = implode('/', $to);
    for($i=0;$i<count($from)-1;$i++)
    {
        array_unshift($to,'..');
    }
    $result = implode('/', $to);
    return $result;
}
Young
  • 7,986
  • 7
  • 43
  • 64
2

Simple one-liner for common scenarios:

str_replace(getcwd() . DIRECTORY_SEPARATOR, '', $filepath)

or:

substr($filepath, strlen(getcwd())+1)

To check if path is absolute, try:

$filepath[0] == DIRECTORY_SEPARATOR
kenorb
  • 155,785
  • 88
  • 678
  • 743
1

Some Reason Gordon's didn't work for me.... Here's my solution

function getRelativePath($from, $to) {
    $patha = explode('/', $from);
    $pathb = explode('/', $to);
    $start_point = count(array_intersect($patha,$pathb));
    while($start_point--) {
        array_shift($patha);
        array_shift($pathb);
    }
    $output = "";
    if(($back_count = count($patha))) {
        while($back_count--) {
            $output .= "../";
        }
    } else {
        $output .= './';
    }
    return $output . implode('/', $pathb);
}
Ceagle
  • 327
  • 7
  • 13
  • Maybe you put a folder path in $from without the trailing slash. Those functions by Gordon and Young require a trailing slash for folders. Unfortunately, your function works with some paths but fail with some others. Please read the test I made in the other post. This function does not seem reliable and should not be used. – lucaferrario Feb 14 '13 at 15:21
1

I came to the same result using those array manipulations :

function getRelativePath($path, $from = __FILE__ )
{
    $path = explode(DIRECTORY_SEPARATOR, $path);
    $from = explode(DIRECTORY_SEPARATOR, dirname($from.'.'));
    $common = array_intersect_assoc($path, $from);

    $base = array('.');
    if ( $pre_fill = count( array_diff_assoc($from, $common) ) ) {
        $base = array_fill(0, $pre_fill, '..');
    }
    $path = array_merge( $base, array_diff_assoc($path, $common) );
    return implode(DIRECTORY_SEPARATOR, $path);
}

The second argument is the file which the path is relative to. It's optional so you can get the relative path regardless the webpage your currently are. In order to use it with @Young or @Gordon example, because you want to know the relative path to $b from $a, you'll have to use

getRelativePath($b, $a);
loranger
  • 702
  • 6
  • 8
  • 1
    Unfortunately, your function works with some paths but fail with some others. Please read the test I made in the other post. This function does not seem reliable and should not be used. Instead, I suggest using Gordon's one, which always returns the correct result. – lucaferrario Feb 14 '13 at 15:27
  • @lucaferrario, please check my [comment](http://stackoverflow.com/questions/2637945/getting-relative-path-from-absolute-path-in-php/14329380#comment21288786_14878054) above – loranger Mar 14 '13 at 10:44
1

Here's what works for me. For some unknown reason, the most upvoted answer to this question didn't work as expected

public function getRelativePath($absolutePathFrom, $absolutePathDestination)
{
    $absolutePathFrom = is_dir($absolutePathFrom) ? rtrim($absolutePathFrom, "\/")."/" : $absolutePathFrom;
    $absolutePathDestination = is_dir($absolutePathDestination) ? rtrim($absolutePathDestination, "\/")."/" : $absolutePathDestination;
    $absolutePathFrom = explode("/", str_replace("\\", "/", $absolutePathFrom));
    $absolutePathDestination = explode("/", str_replace("\\", "/", $absolutePathDestination));
    $relativePath = "";
    $path = array();
    $_key = 0;
    foreach($absolutePathFrom as $key => $value)
    {
        if (strtolower($value) != strtolower($absolutePathDestination[$key]))
        {
            $_key = $key + 1;
            for ($i = $key; $i < count($absolutePathDestination); $i++)
            {
                $path[] = $absolutePathDestination[$i];
            }
            break;
        }
    }
    for ($i = 0; $i <= (count($absolutePathFrom) - $_key - 1); $i++)
    {
        $relativePath .= "../";
    }

    return $relativePath.implode("/", $path);
}

if $a = "C:\xampp\htdocs\projects\SMS\App\www\App\index.php" and
   $b = "C:\xampp\htdocs\projects\SMS\App/www/App/bin/bootstrap/css/bootstrap.min.css"

Then $c, which is the relative path of $b from $a, will be

$c = getRelativePath($a, $b) = "bin/bootstrap/css/bootstrap.min.css"

0

I also encountered that problem, and came to the following solution. I modified the semantics a bit. Arguments ending with a slash is treated as a directory, otherwise as a file, to match the behaviour of browsers when resolving relative urls.

This approach operates directly on strings and is around twice as fast as the fastest array-based solution.

function get_rel_path(string $from, string $to) {
    /* Find position of first difference between the two paths */
    $matchlen = strspn($from ^ $to, "\0");
    /* Search backwards for the next '/' */
    $lastslash = strrpos($from, '/', $matchlen - strlen($from) - 1) + 1;
    /* Count the slashes in $from after that position */
    $countslashes = substr_count($from, '/', $lastslash);

    return str_repeat('../', $countslashes).substr($to, $lastslash)?: './';
}
R. E.
  • 1
  • 1