Using User ID arguments in Drupal menu items

It is a very common need to be able to enter a menu path similar to user/[uid]/profile in the Drupal menu system, ([uid] being a dynamic argument for the user's id) but that's not possible out of the box, and there are no modules which provide this functionality.

Yesterday I got such functionality working by using the often-unknown custom_url_rewrite_outbound() function.

The first step is to implement hook_menu() to define a menu path that will be used as an identifier for all argument-supported paths:

/**
 * Implementation of hook_menu(); 
 */ 
function yuba_menu() {
  $items = array();

  $items['yuba'] = array(
    'title' => 'None',
    'page callback' => 'drupal_goto',
    'page arguments' => array(''),
    'access callback' => TRUE,
    'type' => MENU_CALLBACK,
  );
  
  return $items;
}

What this path actually resolves to (drupal_goto() in this case) doesn't matter - it will never get called. The next part is to add this implementation of custom_url_rewrite_outbound() to your settings.php file:

function custom_url_rewrite_outbound(&$path, &$options, $original_path) {
  /*
    Available tokens:

    user_uid -> currently logged in user
    account_uid -> uid of user being viewed, or node's author
    func_foo_uid -> will call foo_uid(), which should return the appropriate uid
  */

  $tokens = array(
    'user_uid',
    'account_uid',
    'func_(.*)_uid',
  );

  // we don't do path modifications on the /admin pages.
  if (preg_match('#^yuba/(.+('.implode('|', $tokens).').*)$#', $path, $matches) && !preg_match('/^admin/', $_GET['q'])) {
    $path = $matches[1];

    $uid = FALSE;

    switch($matches[2]) {
      // get uid from currently logged-in user
      case 'user_uid':
        global $user;
        $uid = $user->uid;

      break;

      // get uid from user or node being viewed
      case 'account_uid':
        if (preg_match('#user/([0-9]+)#', $_GET['q'], $inner_matches)) {
          $uid = $inner_matches[1];
        }
        elseif (preg_match('#node/([0-9]+)#', $_GET['q'], $inner_matches)) {
          if ($node = node_load($inner_matches[1])) {
           $uid = $node->uid;
          }
        }

      break;

      // let callback function determine uid
      default:
        $func = str_replace('func_', '', $matches[2]);

        if (function_exists($func)) {
          $uid = (int) $func($original_path, $path);
        }

      break;
    }

    if ($uid) {
      $path = str_replace($matches[2], $uid, $path);
    }
  }
}

This code looks for all paths beginning with the 'yuba' prefix (the one we defined in our custom hook), and modifies them, depending on the specified argument.

The available arguments are account_uid, user_uid, and func_my_module_uid.

Examples in paths:

yuba/foo/account_uid/bar
This will replace the token with the UID of the user being viewed (if on a /user/##) page, or if you're on a node page (/node/##), the UID of the node's author
yuba/foo/user_uid
This will replace the token with the UID of the currently logged in user (in the global $user object)
yuba/foo/func_my_module_uid/bar
This will replace the token with whatever value is returned by the function my_module_uid, which will recieve two arguments ($original_path, and $path); this allows for custom functions that determine what UID is to be used.