How to merge paths in PHP properly

Updated on 20.07.2016 with a small fix.

Let’s talk about PHP again.

Suppose you store the base directory path to your PHP app in the $basedir var, then you have uploads directory relative to it and configured in $uploads var and inside that dir every user has its own dir named after his $user_id and finally you know that user has uploaded a picture inside it, the relative path to it is “/my-pictures/me.jpg”.

How do you create an absolute path to that image? The PHP itself allows only one way of concatenating strings:

$full_path = $basedir . '/' . $uploads . '/' . $user_id . '/' . '/my-pictures/me.jpg';

Will that work? The true answer is: most of the time.
If you are using this technique there is a high chance you get a link like this in the end: https://ruslanbes.com/demos/basedir/my-uploads/666//my-pictures/me.jpg
See this double slash thingy? Luckily Apache webserver will understand that you meant one slash and replace it for you, but if you have specific rewrite rules in .htaccess or in you php app this might be a problem and you’ll get 404 error. Another thing happens when you rely on slashes inside your vars and end up with the link like https://ruslanbes.com/demos/basedirmy-uploads/666/my-pictures/me.jpg. All these things are quite annoying, so in my projects I usually follow two simple rules.

Rule 1: All paths should be stored without trailing slashes. If we allow user to enter the path somewhere, we trim the trailing slash immediately before saving that into the database.

Rule 2: It is generally recommended to merge paths using special function instead of concatenation. Here it is:

<?php

/**
 * Merge several parts of URL or filesystem path in one path
 * Examples:
 *  echo merge_paths('stackoverflow.com', 'questions');           // 'stackoverflow.com/questions' (slash added between parts)
 *  echo merge_paths('usr/bin/', '/perl/');                       // 'usr/bin/perl/' (double slashes are removed)
 *  echo merge_paths('en.wikipedia.org/', '/wiki', ' Sega_32X');  // 'en.wikipedia.org/wiki/Sega_32X' (accidental space fixed)
 *  echo merge_paths('etc/apache/', '', '/php');                  // 'etc/apache/php' (empty path element is removed)
 *  echo merge_paths('/', '/webapp/api');                         // '/webapp/api' slash is preserved at the beginnnig
 *  echo merge_paths('http://google.com', '/', '/');              // 'http://google.com/' slash is preserved at the end
 * @param string $path1
 * @param string $path2
 */
function merge_paths($path1, $path2){
    $paths = func_get_args();
    $last_key = func_num_args() - 1;
    array_walk($paths, function(&$val, $key) use ($last_key) {
        switch ($key) {
            case 0:
                $val = rtrim($val, '/ ');
                break;
            case $last_key:
                $val = ltrim($val, '/ ');
                break;
            default:
                $val = trim($val, '/ ');
                break;
        }
    });

    $first = array_shift($paths);
    $last = array_pop($paths);
    $paths = array_filter($paths); // clean empty elements to prevent double slashes
    array_unshift($paths, $first);
    $paths[] = $last;
    return implode('/', $paths);
}

You can test it on eval.in. You may safely include it in your project and put to some common library. I hope something like that will be included into PHP core in future as the problem is very common.

Leave a Reply

Your email address will not be published. Required fields are marked *

This site uses Akismet to reduce spam. Learn how your comment data is processed.