开发者

Getting relative path from absolute path in PHP

I noticed some similar questions about this p开发者_开发问答roblem 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.


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


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


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.


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.


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;
    }


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;
}


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


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);
}


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);


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"


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)?: './';
}
0

上一篇:

下一篇:

精彩评论

暂无评论...
验证码 换一张
取 消

最新问答

问答排行榜