PHP equivalent of Python's `urljoin`
Question: What is the PHP equivalent for building a URL from a base URL and a
potentially-relative path? Python provides
[`urlparse.urljoin`](https://docs.python.org/2/library/urlparse.html#urlparse.urljoin)
but there does not seem to be any standard implementation in PHP.

The closest I've found is people suggesting the use of
[`parse_url`](http://php.net/manual/en/function.parse-url.php) and then
rebuilding the URL from parts, but implementations doing that generally get
things like protocol-relative links wrong (for example, `//example.com/foo`
turning into `http://example.com/foo` or `https://example.com/foo`, inheriting
the base URL's protocol), and it also doesn't make it easy to handle things
like parent directory links. Here are examples of those things working
correctly in `urlparse.urljoin`:

    
    
    >>> from urlparse import urljoin
    >>> urljoin('http://example.com/some/directory/filepart', 'foo.jpg')
    'http://example.com/some/directory/foo.jpg'
    >>> urljoin('http://example.com/some/directory/', 'foo.jpg')
    'http://example.com/some/directory/foo.jpg'
    >>> urljoin('http://example.com/some/directory/', '../foo.jpg')
    'http://example.com/some/foo.jpg'
    >>> urljoin('http://example.com/some/directory/', '/foo.jpg')
    'http://example.com/foo.jpg'
    >>> urljoin('http://example.com/some/directory/', '//images.example.com/bar.jpg')
    'http://images.example.com/bar.jpg'
    >>> urljoin('https://example.com/some/directory/', '//images.example.com/bar.jpg')
    'https://images.example.com/bar.jpg'
    >>> urljoin('ftp://example.com/some/directory/', '//images.example.com/bar.jpg') 
    'ftp://images.example.com/bar.jpg'
    >>> urljoin('http://example.com:8080/some/directory/', '//images.example.com/bar.jpg')
    'http://images.example.com/bar.jpg'
    

Is there an idiomatic way of achieving the same in PHP, or a well-regarded
simple library or implementation that actually gets all of these cases
correct?


Answer: Because there is clearly a need for this functionality and none of the random
scripts out there cover all the bases, I've started a [project on
Github](https://github.com/plaidfluff/php-urljoin) to try to do it right.

The implementation of `urljoin()` is currently as follows:

    
    
    function urljoin($base, $rel) {
        $pbase = parse_url($base);
        $prel = parse_url($rel);
    
        $merged = array_merge($pbase, $prel);
        if ($prel['path'][0] != '/') {
            // Relative path
            $dir = preg_replace('@/[^/]*$@', '', $pbase['path']);
            $merged['path'] = $dir . '/' . $prel['path'];
        }
    
        // Get the path components, and remove the initial empty one
        $pathParts = explode('/', $merged['path']);
        array_shift($pathParts);
    
        $path = [];
        $prevPart = '';
        foreach ($pathParts as $part) {
            if ($part == '..' && count($path) > 0) {
                // Cancel out the parent directory (if there's a parent to cancel)
                $parent = array_pop($path);
                // But if it was also a parent directory, leave it in
                if ($parent == '..') {
                    array_push($path, $parent);
                    array_push($path, $part);
                }
            } else if ($prevPart != '' || ($part != '.' && $part != '')) {
                // Don't include empty or current-directory components
                if ($part == '.') {
                    $part = '';
                }
                array_push($path, $part);
            }
            $prevPart = $part;
        }
        $merged['path'] = '/' . implode('/', $path);
    
        $ret = '';
        if (isset($merged['scheme'])) {
            $ret .= $merged['scheme'] . ':';
        }
    
        if (isset($merged['scheme']) || isset($merged['host'])) {
            $ret .= '//';
        }
    
        if (isset($prel['host'])) {
            $hostSource = $prel;
        } else {
            $hostSource = $pbase;
        }
    
        // username, password, and port are associated with the hostname, not merged
        if (isset($hostSource['host'])) {
            if (isset($hostSource['user'])) {
                $ret .= $hostSource['user'];
                if (isset($hostSource['pass'])) {
                    $ret .= ':' . $hostSource['pass'];
                }
                $ret .= '@';
            }
            $ret .= $hostSource['host'];
            if (isset($hostSource['port'])) {
                $ret .= ':' . $hostSource['port'];
            }
        }
    
        if (isset($merged['path'])) {
            $ret .= $merged['path'];
        }
    
        if (isset($prel['query'])) {
            $ret .= '?' . $prel['query'];
        }
    
        if (isset($prel['fragment'])) {
            $ret .= '#' . $prel['fragment'];
        }
    
    
        return $ret;
    }
    

This function will correctly handle users, passwords, port numbers, query
strings, anchors, and even `file:///` URLs (which seems to be a common defect
in existing functions of this type).

