How can I use tab_parent/tab_root in hook_menu to have one router item appear as a local task for another router item with a different path? Say I have the path widget/123 to view a widget, and have another path to edit some properties of this widget at admin/acme/widget/123/edit. In hook_menu it looks like I can still have the admin path appear as a local task (tab) by setting the tab_parent and tab_root properties in hook_menu(), but I can't seem to get it to work. I know how to setup widget/%/edit as a local task, but in this case I'm only interested in setting a different "root" path as a local task.

Here's my code:

&lt;?php

function acme_menu() {

  // View a widget.
  $items['widget/%'] = array(
    'type' =&gt; MENU_CALLBACK,
    'page callback' =&gt; 'acme_widget_view',
    'page arguments' =&gt; array(1),
    'access callback' =&gt; 'acme_widget_access',
    'access arguments' =&gt; array(1, 'view'),
    'file' =&gt; 'includes/widget.view.inc',
  );

  // Edit a widget.
  $items['admin/acme/widget/%/edit'] = array(
    'type' =&gt; MENU_LOCAL_TASK,
    'tab_parent' =&gt; 'widget/%',
    'tab_root' =&gt; 'widget/%',
    'title' =&gt; 'Edit Widget',
    'page callback' =&gt; 'drupal_get_form',
    'page arguments' =&gt; array('acme_widget_edit', 3),
    'access callback' =&gt; 'acme_widget_access',
    'access arguments' =&gt; array(1, 'edit'),
    'file' =&gt; 'includes/widget.edit.inc',
  );

  return $items;
}

?&gt;


Am I misunderstanding tab_parent/root, or can this be done?

Update: I was missing a MENU_DEFAULT_LOCAK_TASK. Adding

$items['widget/%/view'] = array(
    'type' =&gt; MENU_DEFAULT_LOCAL_TASK, 
    'title' =&gt; 'View'
);


allows the tabs to appear. However, they don't share the same argument position and so the argument (the widget ID) isn't passed between the two. The 'Edit' tab, for example, links to admin/acme/widget//edit. Is there a way to have different routes like this and pass the argument position around?
If I'm not mistaken the tab_root and tab_parent properties need to point to a path prefix of the router item in question - at least if there are wildcards on the path. The code responsible for this restriction is somewhat convoluted, but what you find when stepping through it is the following:

When generating the tab-links, the _menu_translate function is responsible for replacing the %-wildcards with actual values (node-id, etc). However the replacement values are not computed from the actual object-ids but are derived from the current URL. For example when accessing widget/5, the object-id is on position 1 and therefore when generating tab-links, the value 5 is only injected into paths having the %-wildcard at position 1.

This also leads to another consequence. When navigating to admin/acme/widget/5/edit, you will find that there are no tabs on this page - even if widget number 5 actually exists. This is because when generating the URLs for the other tabs, they will start with widget/acme instead of widget/5 because the value acme is at position 1 in the current path.

If you like to provide tabs on your widget page as well as have it in the backend, then simply duplicate the router item. E.g:

function acme_menu() {

  // View a widget.
  $items['widget/%'] = array(
    'type' =&gt; MENU_CALLBACK,
    'page callback' =&gt; 'acme_widget_view',
    'page arguments' =&gt; array(1),
    'access callback' =&gt; 'acme_widget_access',
    'access arguments' =&gt; array(1, 'view'),
    'file' =&gt; 'includes/widget.view.inc',
  );

  $items['widget/%/view'] = array(
    'type' =&gt; MENU_DEFAULT_LOCAL_TASK,
    'title' =&gt; 'View'
  );

  // Edit a widget - frontend
  $items['widget/%/edit'] = array(
    'type' =&gt; MENU_LOCAL_TASK,
    'title' =&gt; 'Edit Widget',
    'page callback' =&gt; 'drupal_get_form',
    'page arguments' =&gt; array('acme_widget_edit', 1),
    'access callback' =&gt; 'acme_widget_access',
    'access arguments' =&gt; array(1, 'edit'),
    'file' =&gt; 'includes/widget.edit.inc',
  );

  // Edit a widget - backend
  $items['admin/acme/widget/%/edit'] = array(
    'type' =&gt; MENU_LOCAL_TASK,
    'title' =&gt; 'Edit Widget',
    'page callback' =&gt; 'drupal_get_form',
    'page arguments' =&gt; array('acme_widget_edit', 3),
    'access callback' =&gt; 'acme_widget_access',
    'access arguments' =&gt; array(3, 'edit'),
    'file' =&gt; 'includes/widget.edit.inc',
  );

  return $items;
}


If you like to move around stuff from a third-party module, you still can implement additional router-items on behalf of it in a custom module - or even move them using hook_menu_alter.