Converting Absolute Paths to Relative Paths in PHP

In PHP code, have you ever needed to convert an absolute path to a path that's relative to another path? If you look through the PHP function library, you'll find that there doesn't seem to be a function to do this.

Luckily, RedBottle is here to make your day! Here's an implementation of a utility function to convert absolute paths to relative paths, licensed under the GNU GPL v3:

<?php
/**
 * @file
 *  <p>The <code>absolute_to_relative_path()</code> utility function.</p>
 *
 *  <p>© 2011 RedBottle Design, LLC. All rights reserved.</p>
 *
 *  <p>http://www.redbottledesign.com</p>
 *
 *  <p>This source code is free software: you can redistribute it and/or modify
 *  it under the terms of the Lesser GNU General Public License as published by
 *  the Free Software Foundation, either version 3 of the License, or
 *  (at your option) any later version.</p>
 *
 *  <p>This program is distributed in the hope that it will be useful,
 *  but WITHOUT ANY WARRANTY; without even the implied warranty of
 *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 *  Lesser GNU General Public License for more details.</p>
 *
 *  <p>You should have received a copy of the Lesser GNU General Public License
 *  along with this program.  If not, see <http://www.gnu.org/licenses/>.</p>
 *
 * @author Guy Paddock (guy.paddock@redbottledesign.com)
 */

  /**
   * Convert an absolute path to a path relative to another absolute path.
   *
   * @param   string  $from_path
   *                  The starting absolute path.
   *
   * @param   string  $to_path
   *                  The ending absolute path.
   *
   * @return  string  The relative path for navigating from
   *                  <code>$from_path</code> to <code>$to_path</code>.
   *
   * @throws          InvalidArgumentException
   *                  If <code>$from_path</code> or <code>$to_path</code> are
   *                  NULL, empty, or not absolute paths.
   */
  function absolute_to_relative_path($from_path, $to_path) {
    if (empty($to_path)) {
      throw new InvalidArgumentException('$to_path must not be NULL or empty.');
    }

    if ($to_path[0] !== '/') {
      throw new InvalidArgumentException('$to_path must be an absolute path.');
    }

    if (empty($from_path)) {
      throw new InvalidArgumentException('$from_path must not be NULL or empty.');
    }

    if ($from_path[0] !== '/') {
      throw new InvalidArgumentException('$from_path must be an absolute path.');
    }

    // Trim off trailing slashes for consistency, then turn paths into arrays
    $to_path_parts    = explode('/', rtrim($to_path, '/'));
    $from_path_parts  = explode('/', rtrim($from_path, '/'));

    $common_part_count = 0;

    /* Count how many parts the two paths have in common, since those parts
     * aren't included in the relative path.
     */
    for ($i = 0; $i < max(sizeof($to_path_parts), sizeof($from_path_parts)); ++$i) {
      if (isset($to_path_parts[$i]) && isset($from_path_parts[$i])) {
        if ($to_path_parts[$i] == $from_path_parts[$i]) {
          ++$common_part_count;
        }
        else {
          break;
        }
      }

      else {
        break;
      }
    }

    $relative_parts = array();

    /* Each part of the "from path" that remains after the common parts has to
     * be replaced with ".." to get back to the common root for both paths.
     */
    if (sizeof($from_path_parts) > $common_part_count) {
      $replacement_count  = sizeof($from_path_parts) - $common_part_count;
      $relative_parts     = array_fill(0, $replacement_count, '..');
    }

    /*
     * Each part of the "to path" that remains after the common parts is merely
     * appended to the relative path.
     */
    if (sizeof($to_path_parts) > $common_part_count) {
      $remaining_to_path_parts  = array_slice($to_path_parts, $common_part_count);
      $relative_parts           = array_merge($relative_parts, $remaining_to_path_parts);
    }

    // Turn array back into path string
    return implode('/', $relative_parts);
  }
?>

Here's a simple set of test cases that demonstrate the function:

<?php
  $paths = array(
    array(
      '/path/a/b/c',
      '/path/d/e',
      '../../../d/e'
    ),

    array(
      '/path/d/e',
      '/path/a/b/c',
      '../../a/b/c'
    ),

    array(
      '/a/b/c',
      '/',
      '../../..'
    ),

    array(
      '/',
      '/a/b/c',
      'a/b/c',
    ),

    array(
      '/a/b/c',
      '/a/b/c/d/e',
      'd/e'
    ),
  );

  foreach ($paths as $path_set) {
    $from_path        = $path_set[0];
    $to_path          = $path_set[1];
    $expected_result  = $path_set[2];

    $result = absolute_to_relative_path($from_path, $to_path);

    print "From path: $from_path\n";
    print "To path: $to_path\n";
    print "Result: " . $result . "\n";
    print "Test result: " . (($result == $expected_result) ? 'PASS' : 'FAIL') . "\n\n";

  }
?>