Getting relative path from absolute path in PHP
Asked Answered
D

11

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.

Domingo answered 14/4, 2010 at 13:58 Comment(6)
What's your use case for needing a relative path when you have the real path?Namtar
Could you please post those similar questions? Writing port for PHP is easier that reinviting everything.Domel
@Tim Lytle,I think it maybe some sense when do some link/include stuff.And also for interest.Domingo
For those interested in a PowerShell port, I've written one: #13240120Renfrew
Several answers have been proposed. I benchmarked them all, so please read my post below before deciding which one to use ;-)Manville
Useful for creating symlinks.Aporia
S
73

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
Splash answered 14/4, 2010 at 14:39 Comment(6)
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).Atalanta
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! ;-)Manville
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 sandbox.onlinephpfunctions.com/code/…Intermediate
@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.Splash
@Splash 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 filesystemIntermediate
@EdwinRodríguez well, then don't use the function. It doesn't work in your specific scenario.Splash
M
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
Manville answered 14/2, 2013 at 15:18 Comment(2)
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 ?Corrasion
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! :-)Manville
A
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.

Atalanta answered 14/4, 2010 at 14:31 Comment(0)
P
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.

Population answered 2/12, 2013 at 23:34 Comment(1)
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.Tetratomic
U
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;
    }
Underfur answered 2/1, 2014 at 6:45 Comment(0)
D
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;
}
Domingo answered 15/4, 2010 at 2:4 Comment(0)
K
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
Kreplach answered 11/3, 2015 at 19:23 Comment(0)
T
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);
}
Trespass answered 28/9, 2012 at 8:16 Comment(1)
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.Manville
C
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);
Corrasion answered 15/1, 2013 at 0:42 Comment(2)
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.Manville
@lucaferrario, please check my comment aboveCorrasion
A
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"

Arran answered 24/11, 2015 at 5:56 Comment(0)
C
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)?: './';
}
Contend answered 23/9, 2022 at 8:49 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.