How to join filesystem path strings in PHP?

PhpStringFile

Php Problem Overview


Is there a builtin function in PHP to intelligently join path strings? The function, given abc/de/ and /fg/x.php as arguments, should return abc/de/fg/x.php; the same result should be given using abc/de and fg/x.php as arguments for that function.

If not, is there an available class? It could also be valuable for splitting paths or removing parts of them. If you have written something, may you share your code here?

It is ok to always use /, I am coding for Linux only.

In Python there is os.path.join, which is great.

Php Solutions


Solution 1 - Php

function join_paths() {
    $paths = array();
    
    foreach (func_get_args() as $arg) {
        if ($arg !== '') { $paths[] = $arg; }
    }
    
    return preg_replace('#/+#','/',join('/', $paths));
}

My solution is simpler and more similar to the way Python os.path.join works

Consider these test cases

array               my version    @deceze      @david_miller    @mark

['','']             ''            ''           '/'              '/'
['','/']            '/'           ''           '/'              '/'
['/','a']           '/a'          'a'          '//a'            '/a'
['/','/a']          '/a'          'a'          '//a'            '//a'
['abc','def']       'abc/def'     'abc/def'    'abc/def'        'abc/def'
['abc','/def']      'abc/def'     'abc/def'    'abc/def'        'abc//def'
['/abc','def']      '/abc/def'    'abc/def'    '/abc/def'       '/abc/def'
['','foo.jpg']      'foo.jpg'     'foo.jpg'    '/foo.jpg'       '/foo.jpg'
['dir','0','a.jpg'] 'dir/0/a.jpg' 'dir/a.jpg'  'dir/0/a.jpg'    'dir/0/a.txt'

Solution 2 - Php

> Since this seems to be a popular question and the comments are filling with "features suggestions" or "bug reports"... All this code snippet does is join two strings with a slash without duplicating slashes between them. That's all. No more, no less. It does not evaluate actual paths on the hard disk nor does it actually keep the beginning slash (add that back in if needed, at least you can be sure this code always returns a string without starting slash).

join('/', array(trim("abc/de/", '/'), trim("/fg/x.php", '/')));

The end result will always be a path with no slashes at the beginning or end and no double slashes within. Feel free to make a function out of that.

EDIT: Here's a nice flexible function wrapper for above snippet. You can pass as many path snippets as you want, either as array or separate arguments:

function joinPaths() {
    $args = func_get_args();
    $paths = array();
    foreach ($args as $arg) {
        $paths = array_merge($paths, (array)$arg);
    }

    $paths = array_map(create_function('$p', 'return trim($p, "/");'), $paths);
    $paths = array_filter($paths);
    return join('/', $paths);
}

echo joinPaths(array('my/path', 'is', '/an/array'));
//or
echo joinPaths('my/paths/', '/are/', 'a/r/g/u/m/e/n/t/s/');

:o)

Solution 3 - Php

@deceze's function doesn't keep the leading / when trying to join a path that starts with a Unix absolute path, e.g. joinPaths('/var/www', '/vhosts/site');.

function unix_path() {
  $args = func_get_args();
  $paths = array();

  foreach($args as $arg) {
    $paths = array_merge($paths, (array)$arg);
  }

  foreach($paths as &$path) {
    $path = trim($path, '/');
  }

  if (substr($args[0], 0, 1) == '/') {
    $paths[0] = '/' . $paths[0];
  }
  
  return join('/', $paths);
}

Solution 4 - Php

My take:

function trimds($s) {
	return rtrim($s,DIRECTORY_SEPARATOR);
}

function joinpaths() {
	return implode(DIRECTORY_SEPARATOR, array_map('trimds', func_get_args()));
}

I'd have used an anonymous function for trimds, but older versions of PHP don't support it.

Example:

join_paths('a','\\b','/c','d/','/e/','f.jpg'); // a\b\c\d\e\f.jpg (on Windows)

Updated April 2013 March 2014 May 2018:

function join_paths(...$paths) {
    return preg_replace('~[/\\\\]+~', DIRECTORY_SEPARATOR, implode(DIRECTORY_SEPARATOR, $paths));
}

This one will correct any slashes to match your OS, won't remove a leading slash, and clean up and multiple slashes in a row.

Solution 5 - Php

If you know the file/directory exists, you can add extra slashes (that may be unnecessary), then call realpath, i.e.

realpath(join('/', $parts));

This is of course not quite the same thing as the Python version, but in many cases may be good enough.

Solution 6 - Php

As a fun project, I created yet another solution. Should be universal for all operating systems.

For PHP 7.2+:

<?php

/**
 * Join string into a single URL string.
 *
 * @param string $parts,... The parts of the URL to join.
 * @return string The URL string.
 */
function join_paths(...$parts) {
    if (sizeof($parts) === 0) return '';
    $prefix = ($parts[0] === DIRECTORY_SEPARATOR) ? DIRECTORY_SEPARATOR : '';
    $processed = array_filter(array_map(function ($part) {
        return rtrim($part, DIRECTORY_SEPARATOR);
    }, $parts), function ($part) {
        return !empty($part);
    });
    return $prefix . implode(DIRECTORY_SEPARATOR, $processed);
}

For PHP version before 7.2:

/**
 * Join string into a single URL string.
 *
 * @param string $parts,... The parts of the URL to join.
 * @return string The URL string.
 */
function join_paths() {
    $parts = func_get_args();
    if (sizeof($parts) === 0) return '';
    $prefix = ($parts[0] === DIRECTORY_SEPARATOR) ? DIRECTORY_SEPARATOR : '';
    $processed = array_filter(array_map(function ($part) {
        return rtrim($part, DIRECTORY_SEPARATOR);
    }, $parts), function ($part) {
        return !empty($part);
    });
    return $prefix . implode(DIRECTORY_SEPARATOR, $processed);
}

Some test case for its behaviour.

// relative paths
var_dump(join_paths('hello/', 'world'));
var_dump(join_paths('hello', 'world'));
var_dump(join_paths('hello', '', 'world'));
var_dump(join_paths('', 'hello/world'));
echo "\n";

// absolute paths
var_dump(join_paths('/hello/', 'world'));
var_dump(join_paths('/hello', 'world'));
var_dump(join_paths('/hello/', '', 'world'));
var_dump(join_paths('/hello', '', 'world'));
var_dump(join_paths('', '/hello/world'));
var_dump(join_paths('/', 'hello/world'));

Results:

string(11) "hello/world"
string(11) "hello/world"
string(11) "hello/world"
string(11) "hello/world"

string(12) "/hello/world"
string(12) "/hello/world"
string(12) "/hello/world"
string(12) "/hello/world"
string(12) "/hello/world"
string(12) "/hello/world"

Update: Added a version that supports PHP before 7.2.

Solution 7 - Php

An alternative is using implode() and explode().

$a = '/a/bc/def/';
$b = '/q/rs/tuv/path.xml';

$path = implode('/',array_filter(explode('/', $a . $b)));

echo $path;  // -> a/bc/def/q/rs/tuv/path.xml

Solution 8 - Php

The solution below uses the logic proposed by @RiccardoGalli, but is improved to avail itself of the DIRECTORY_SEPARATOR constant, as @Qix and @FélixSaparelli suggested, and, more important, to trim each given element to avoid space-only folder names appearing in the final path (it was a requirement in my case).

Regarding the escape of directory separator inside the preg_replace() pattern, as you can see I used the preg_quote() function which does the job fine.
Furthermore, I would replace mutiple separators only (RegExp quantifier {2,}).

// PHP 7.+
function paths_join(string ...$parts): string {
$parts = array_map('trim', $parts);
$path = [];



foreach ($parts as $part) {
	if ($part !== '') {
		$path[] = $part;
	}
}

$path = implode(DIRECTORY_SEPARATOR, $path);

return preg_replace(
	'#' . preg_quote(DIRECTORY_SEPARATOR) . '{2,}#',
	DIRECTORY_SEPARATOR,
	$path
);




}

}

Solution 9 - Php

Elegant Python-inspired PHP one-liner way to join path.

This code doesn't use unnecessary array.

Multi-platform
function os_path_join(...$parts) {
  return preg_replace('#'.DIRECTORY_SEPARATOR.'+#', DIRECTORY_SEPARATOR, implode(DIRECTORY_SEPARATOR, array_filter($parts)));
}
Unix based systems
function os_path_join(...$parts) {
  return preg_replace('#/+#', '/', implode('/', array_filter($parts)));
}
Unix based system without REST parameters (don't respect explicit PEP8 philosophy) :
function os_path_join() {
  return preg_replace('#/+#', '/', implode('/', array_filter(func_get_args())));
}

Usage

$path = os_path_join("", "/", "mydir/", "/here/");
Bonus : if you want really follow Python os.path.join(). First argument is required :
function os_path_join($path=null, ...$paths) {
  if (!is_null($path)) {
    throw new Exception("TypeError: join() missing 1 required positional argument: 'path'", 1);
  }
  $path = rtrim($path, DIRECTORY_SEPARATOR);
  foreach ($paths as $key => $current_path) {
    $paths[$key] = $paths[$key] = trim($current_path, DIRECTORY_SEPARATOR);
  }
  return implode(DIRECTORY_SEPARATOR, array_merge([$path], array_filter($paths)));
}

Check os.path.join() source if you want : https://github.com/python/cpython/blob/master/Lib/ntpath.py

Warning : This solution is not suitable for urls.

Solution 10 - Php

for getting parts of paths you can use pathinfo http://nz2.php.net/manual/en/function.pathinfo.php

for joining the response from @deceze looks fine

Solution 11 - Php

A different way of attacking this one:

function joinPaths() {
  $paths = array_filter(func_get_args());
  return preg_replace('#/{2,}#', '/', implode('/', $paths));
}

Solution 12 - Php

This is a corrected version of the function posted by deceze. Without this change, joinPaths('', 'foo.jpg') becomes '/foo.jpg'

function joinPaths() {
	$args = func_get_args();
	$paths = array();
	foreach ($args as $arg)
		$paths = array_merge($paths, (array)$arg);
		
	$paths2 = array();
	foreach ($paths as $i=>$path)
	{   $path = trim($path, '/');
		if (strlen($path))
			$paths2[]= $path;
	}
	$result = join('/', $paths2); // If first element of old path was absolute, make this one absolute also
	if (strlen($paths[0]) && substr($paths[0], 0, 1) == '/')
		return '/'.$result;
	return $result;
}

Solution 13 - Php

This seems to be work quite well, and looks reasonably neat to me.

private function JoinPaths() {
  $slash = DIRECTORY_SEPARATOR;
  $sections = preg_split(
          "@[/\\\\]@",
          implode('/', func_get_args()),
          null,
          PREG_SPLIT_NO_EMPTY);
  return implode($slash, $sections);
}

Solution 14 - Php

Best solution found:

function joinPaths($leftHandSide, $rightHandSide) { 
    return rtrim($leftHandSide, '/') .'/'. ltrim($rightHandSide, '/'); 
}

NOTE: Copied from the comment by user89021

Solution 15 - Php

OS-independent version based on the answer by mpen but encapsulated into a single function and with the option to add a trailing path separator.

function joinPathParts($parts, $trailingSeparator = false){
    return implode(
        DIRECTORY_SEPARATOR, 
        array_map(
            function($s){
                return rtrim($s,DIRECTORY_SEPARATOR);
            }, 
            $parts)
        )
        .($trailingSeparator ? DIRECTORY_SEPARATOR : '');
}

Or for you one-liner lovers:

function joinPathParts($parts, $trailingSeparator = false){
    return implode(DIRECTORY_SEPARATOR, array_map(function($s){return rtrim($s,DIRECTORY_SEPARATOR);}, $parts)).($trailingSeparator ? DIRECTORY_SEPARATOR : '');
}

Simply call it with an array of path parts:

// No trailing separator - ex. C:\www\logs\myscript.txt
$logFile = joinPathParts([getcwd(), 'logs', 'myscript.txt']);

// Trailing separator - ex. C:\www\download\images\user1234\
$dir = joinPathParts([getcwd(), 'download', 'images', 'user1234'], true);

Solution 16 - Php

Here's a function that behaves like Node's path.resolve:

function resolve_path() {
    $working_dir = getcwd();
    foreach(func_get_args() as $p) {
        if($p === null || $p === '') continue;
        elseif($p[0] === '/') $working_dir = $p;
        else $working_dir .= "/$p";
    }
    $working_dir = preg_replace('~/{2,}~','/', $working_dir);
    if($working_dir === '/') return '/';
    $out = [];
    foreach(explode('/',rtrim($working_dir,'/')) as $p) {
        if($p === '.') continue;
        if($p === '..') array_pop($out);
        else $out[] = $p;
    }
    return implode('/',$out);
}

Test cases:

resolve_path('/foo/bar','./baz')         # /foo/bar/baz
resolve_path('/foo/bar','/tmp/file/')    # /tmp/file
resolve_path('/foo/bar','/tmp','file')   # /tmp/file
resolve_path('/foo//bar/../baz')         # /foo/baz
resolve_path('/','foo')                  # /foo
resolve_path('/','foo','/')              # /
resolve_path('wwwroot', 'static_files/png/', '../gif/image.gif') 
                                  # __DIR__.'/wwwroot/static_files/gif/image.gif'

Solution 17 - Php

From the great answer of Ricardo Galli, a bit of improvement to avoid killing the protocol prefix.

The idea is to test for the presence of a protocol in one argument, and maintain it into the result. WARNING: this is a naive implementation!

For example:

array("http://domain.de","/a","/b/")

results to (keeping protocol)

"http://domain.de/a/b/"

instead of (killing protocol)

"http:/domain.de/a/b/"

But http://codepad.org/hzpWmpzk needs a better code writing skill.

Solution 18 - Php

I love Riccardo's answer and I think it is the best answer.

I am using it to join paths in url building, but with one small change to handle protocols' double slash:

function joinPath () {
	$paths = array();

	foreach (func_get_args() as $arg) {
		if ($arg !== '') { $paths[] = $arg; }
	}

	// Replace the slash with DIRECTORY_SEPARATOR
	$paths = preg_replace('#/+#', '/', join('/', $paths));
	return preg_replace('#:/#', '://', $paths);
}

Solution 19 - Php

function path_combine($paths) {
  for ($i = 0; $i < count($paths); ++$i) {
    $paths[$i] = trim($paths[$i]);
  }

  $dirty_paths = explode(DIRECTORY_SEPARATOR, join(DIRECTORY_SEPARATOR, $paths));
  for ($i = 0; $i < count($dirty_paths); ++$i) {
    $dirty_paths[$i] = trim($dirty_paths[$i]);
  }

  $unslashed_paths = array();

  for ($i = 0; $i < count($dirty_paths); ++$i) {
    $path = $dirty_paths[$i];
    if (strlen($path) == 0) continue;
    array_push($unslashed_paths, $path);
  }

  $first_not_empty_index = 0;
  while(strlen($paths[$first_not_empty_index]) == 0) {
    ++$first_not_empty_index;
  }
  $starts_with_slash = $paths[$first_not_empty_index][0] == DIRECTORY_SEPARATOR;

  return $starts_with_slash
    ? DIRECTORY_SEPARATOR . join(DIRECTORY_SEPARATOR, $unslashed_paths)
    : join(DIRECTORY_SEPARATOR, $unslashed_paths);
}

Example usage:

$test = path_combine([' ', '/cosecheamo', 'pizze', '///// 4formaggi', 'GORGONZOLA']);
echo $test;

Will output:

/cosecheamo/pizze/4formaggi/GORGONZOLA

Solution 20 - Php

Here is my solution:

function joinPath(): string {
		
		$path = '';
		foreach (func_get_args() as $numArg => $arg) {
			
			$arg = trim($arg);

			$firstChar = substr($arg, 0, 1);
			$lastChar = substr($arg, -1);

			if ($numArg != 0 && $firstChar != '/') {
				$arg = '/'.$arg;
				}
			
			# Eliminamos el slash del final
			if ($lastChar == '/') {
				$arg = rtrim($arg, '/');
				}
				
			$path .= $arg;
			}
		
		return $path;
		}

Solution 21 - Php

Hmmm most seem a bit over complicated. Dunno, this is my take on it:

// Takes any amount of arguments, joins them, then replaces double slashes
function join_urls() {
   $parts = func_get_args();
   $url_part = implode("/", $parts);
   return preg_replace('/\/{1,}/', '/', $url_part);
}

Solution 22 - Php

For people who want a join function that does the Windows backslash and the Linux forward slash.

Usage:

<?php
use App\Util\Paths
echo Paths::join('a','b'); //Prints 'a/b' on *nix, or 'a\\b' on Windows

Class file:

<?php
namespace App\Util;

class Paths
{
  public static function join_with_separator($separator, $paths) {
    $slash_delimited_path = preg_replace('#\\\\#','/', join('/', $paths));
    $duplicates_cleaned_path = preg_replace('#/+#', $separator, $slash_delimited_path);
    return $duplicates_cleaned_path;
  }

  public static function join() {
    $paths = array();

    foreach (func_get_args() as $arg) {
      if ($arg !== '') { $paths[] = $arg; }
    }
    return Paths::join_with_separator(DIRECTORY_SEPARATOR, $paths);
  }
}

Here's the test function:

<?php

namespace Tests\Unit;

use PHPUnit\Framework\TestCase;
use App\Util\Paths;

class PathsTest extends TestCase
{
  public function testWindowsPaths()
  {
    $TEST_INPUTS = [
      [],
      ['a'],
      ['a','b'],
      ['C:\\','blah.txt'],
      ['C:\\subdir','blah.txt'],
      ['C:\\subdir\\','blah.txt'],
      ['C:\\subdir','nested','1/2','blah.txt'],
    ];
    $EXPECTED_OUTPUTS = [
      '',
      'a',
      'a\\b',
      'C:\\blah.txt',
      'C:\\subdir\\blah.txt',
      'C:\\subdir\\blah.txt',
      'C:\\subdir\\nested\\1\\2\\blah.txt',
    ];
    for ($i = 0; $i < count($TEST_INPUTS); $i++) {
      $actualPath = Paths::join_with_separator('\\', $TEST_INPUTS[$i]);
      $expectedPath = $EXPECTED_OUTPUTS[$i];
      $this->assertEquals($expectedPath, $actualPath);
    }
  }
  public function testNixPaths()
  {
    $TEST_INPUTS = [
      [],
      ['a'],
      ['a','b'],
      ['/home','blah.txt'],
      ['/home/username','blah.txt'],
      ['/home/username/','blah.txt'],
      ['/home/subdir','nested','1\\2','blah.txt'],
    ];
    $EXPECTED_OUTPUTS = [
      '',
      'a',
      'a/b',
      '/home/blah.txt',
      '/home/username/blah.txt',
      '/home/username/blah.txt',
      '/home/subdir/nested/1/2/blah.txt',
    ];
    for ($i = 0; $i < count($TEST_INPUTS); $i++) {
      $actualPath = Paths::join_with_separator('/', $TEST_INPUTS[$i]);
      $expectedPath = $EXPECTED_OUTPUTS[$i];
      $this->assertEquals($expectedPath, $actualPath);
    }
  }
}

Solution 23 - Php

I liked several solutions presented. But those who does replacing all '/+' into '/' (regular expressions) are forgetting that os.path.join() from python can handle this kind of join:

os.path.join('http://example.com/parent/path', 'subdir/file.html')

Result: 'http://example.com/parent/path/subdir/file.html';

Attributions

All content for this solution is sourced from the original question on Stackoverflow.

The content on this page is licensed under the Attribution-ShareAlike 4.0 International (CC BY-SA 4.0) license.

Content TypeOriginal AuthorOriginal Content on Stackoverflow
Questionuser89021View Question on Stackoverflow
Solution 1 - PhpRiccardo GalliView Answer on Stackoverflow
Solution 2 - PhpdecezeView Answer on Stackoverflow
Solution 3 - PhpDavid MillerView Answer on Stackoverflow
Solution 4 - PhpmpenView Answer on Stackoverflow
Solution 5 - PhpGeorge LundView Answer on Stackoverflow
Solution 6 - PhpKoala YeungView Answer on Stackoverflow
Solution 7 - PhpChris JView Answer on Stackoverflow
Solution 8 - PhpyodabarView Answer on Stackoverflow
Solution 9 - PhpSamuel DauzonView Answer on Stackoverflow
Solution 10 - PhpbumperboxView Answer on Stackoverflow
Solution 11 - PhpstompydanView Answer on Stackoverflow
Solution 12 - PhpEricPView Answer on Stackoverflow
Solution 13 - PhpKenny HungView Answer on Stackoverflow
Solution 14 - PhpBasil MusaView Answer on Stackoverflow
Solution 15 - PhpMagnusView Answer on Stackoverflow
Solution 16 - PhpmpenView Answer on Stackoverflow
Solution 17 - PhpnicolalliasView Answer on Stackoverflow
Solution 18 - PhpSmilyView Answer on Stackoverflow
Solution 19 - Phpuser6307854View Answer on Stackoverflow
Solution 20 - PhpDanielView Answer on Stackoverflow
Solution 21 - PhpRudi StrydomView Answer on Stackoverflow
Solution 22 - PhpturiyagView Answer on Stackoverflow
Solution 23 - PhpRoger DemetrescuView Answer on Stackoverflow