Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

PHP concatenation of paths

Is there a PHP internal function for path concatenation ? What possibilities do I have to merge several paths (absolute and relative).

//Example: 
$path1="/usr/home/username/www";
$path2="domainname";
$path3="images/thumbnails";
$domain="exampledomain.com";

//As example: Now I want to create a new path (domain + path3) on the fly. 
$result = $domain.DIRECTORY_SEPARATOR.$path3

Ok, there is an easy solution for this example, but what if there are different dictionary separators or some paths are a little bit more complicated?

Is there an existing solution for trim it like this: /home/uploads/../uploads/tmp => /home/uploads/tmp ....

And how would a platform-independent version of an path-concat-function look like?

should an relative path start with "./" as prefix or is "home/path/img/" the common way?

like image 206
NaN Avatar asked Jan 20 '23 03:01

NaN


1 Answers

I ran into this problem myself, primarily regarding the normalization of paths.

Normalization is:

  • One separator (I've chosen to support, but never return a backwards slash \\)
  • Resolving indirection: /../
  • Removing duplicate separators: /home/www/uploads//file.ext
  • Always remove trailing separator.

I've written a function that achieves this. I don't have access to that code right now, but it's also not that hard to write it yourself.

Whether a path is absolute or not doesn't really matter for the implementation of this normalization function, just watch out for the leading separator and you're good.

I'm not too worried about OS dependence. Both Windows and Linux PHP understand / so for the sake of simplicity I'm just always using that - but I guess it doesn't really matter what separator you use.


To answer your question: path concatenation can be very easy if you just always use / and assume that a directory has no trailing separator. 'no trailing separator' seems like a good assumption because functions like dirname remove the trailing separator.

Then it's always safe to do: $dir . "/" . $file.

And even if the result path is /home/uploads/../uploads//my_uploads/myfile.ext it's still going to work fine.

Normalization becomes useful when you need to store the path somewhere. And because you have this normalization function you can make these assumptions.


An additional useful function is a function to make relative paths.

  • /files/uploads
  • /files/uploads/my_uploads/myfile.ext

It can be useful to derive from those two paths, what the relative path to the file is.


realpath

I've found realpath to be extremely performance heavy. It's not so bad if you're calling it once but if you're doing it in a loop somewhere you get a pretty big hit. Keep in mind that each realpath call is a call to the filesystem as well. Also, it will simply return false if you pass in something silly, I'd rather have it throw an Exception.

To me the realpath function is a good example of a BAD function because it does two things: 1. It normalizes the path and 2. it checks if the path exists. Both of these functions are useful of course but they must be separated. It also doesn't distinguish between files and directories. For windows this typically isn't a problem, but for Linux it can be.

And I think there is some quirky-ness when using realpath("") on Windows. I think it will return \\ - which can be profoundly unacceptable.


/**
 * This function is a proper replacement for realpath
 * It will _only_ normalize the path and resolve indirections (.. and .)
 * Normalization includes:
 * - directiory separator is always /
 * - there is never a trailing directory separator
 * @param  $path
 * @return String
 */
function normalize_path($path) {
    $parts = preg_split(":[\\\/]:", $path); // split on known directory separators

    // resolve relative paths
    for ($i = 0; $i < count($parts); $i +=1) {
        if ($parts[$i] === "..") {          // resolve ..
            if ($i === 0) {
                throw new Exception("Cannot resolve path, path seems invalid: `" . $path . "`");
            }
            unset($parts[$i - 1]);
            unset($parts[$i]);
            $parts = array_values($parts);
            $i -= 2;
        } else if ($parts[$i] === ".") {    // resolve .
            unset($parts[$i]);
            $parts = array_values($parts);
            $i -= 1;
        }
        if ($i > 0 && $parts[$i] === "") {  // remove empty parts
            unset($parts[$i]);
            $parts = array_values($parts);
        }
    }
    return implode("/", $parts);
}

/**
 * Removes base path from longer path. The resulting path will never contain a leading directory separator
 * Base path must occur in longer path
 * Paths will be normalized
 * @throws Exception
 * @param  $base_path
 * @param  $longer_path
 * @return string normalized relative path
 */
function make_relative_path($base_path, $longer_path) {
    $base_path = normalize_path($base_path);
    $longer_path = normalize_path($longer_path);
    if (0 !== strpos($longer_path, $base_path)) {
        throw new Exception("Can not make relative path, base path does not occur at 0 in longer path: `" . $base_path . "`, `" . $longer_path . "`");
    }
    return substr($longer_path, strlen($base_path) + 1);
}
like image 70
Halcyon Avatar answered Jan 23 '23 01:01

Halcyon