Adding a tab to user profiles in Drupal 7

Have you ever tried adding a tab to the user account edit form? Certain modules like linkedin, profile2, and twitter have managed to do it, but I had never done any research into how they were doing it. I just assumed that it was a hook_menu of type MENU_LOCAL_TASK. So I did what most Drupal developers do when they need to create a new page. I called upon hook menu. Except almost everything I tried didn’t add a tab to the user edit form in the way that I wanted it to. The first thing I tried was this:

<?php
 
/**
 * Implements hook_menu().
 */
function mymodule_hook_menu() {
  $items['user/%user/new-tab'] = array(
    'title' => t('This is a new profile tab'),
    'page callback' => 'drupal_get_form',
    'page arguments' => array('mymodule_custom_form', 1), // Uid
    'access callback' => 'user_edit_access',
    'access arguments' => array(1),
    'type' => MENU_LOCAL_TASK,
    'file' => 'mymodule.form.inc',
  );
}

That adds an item on the **primary** level of tabs, but that wasn’t what I needed. I needed to add an item to the secondary level of tabs so that when a user edits their profile they see two options, ‘account’ and ‘new-tab’. After some more fiddling I finally got it to work:

<?php
 
/**
 * Implements hook_menu().
 */
function mymodule_hook_menu() {
  $items['user/%user/edit/new-tab'] = array(
    'title' => t('This is a new profile tab'),
    'page callback' => 'drupal_get_form',
    'page arguments' => array('mymodule_custom_form', 1), // Uid
    'access callback' => 'user_edit_access',
    'access arguments' => array(1),
    'type' => MENU_LOCAL_TASK,
    'file' => 'mymodule.form.inc',
  );
}

Woohoo, account and new tab are both showing on user/[uid]/edit! Did you notice the change? It’s a pretty small one. Let me just click new-tab and check out my form on the other tab. *click* Wait a second…where did the tabs go? I should have known since user/[uid]/edit/new-tab is not on the same level as user/[uid]/edit. Hmm now I’m really stuck. After a few Google sessions and going through user.module I found the real way to do it. It is completely different from what I expected it to be. Here’s the way that you actually add an item to the user profile edit form.

<?php
 
/**
 * Implements hook_user_categories() to register a secondary tab on
 * user edit form.
 */
function mymodule_user_categories() {
  return array(
    array(
      'name' => 'new-tab',
      'title' => 'This is a new profile tab',
      'weight' => 2,
    ),
  );
}
 
/**
 * Implements hook_menu_alter() to change callbacks of the new tab
 */
function mymodule_menu_alter(&$callbacks) {
  $callbacks['user/%user_category/edit/new-tab']['page callback'] = 'mymodule_callback_function';
  // Not sure if this next line is totally necessary
  $callbacks['user/%user_category/edit/new-tab']['module'] = 'mymodule';
  $callbacks['user/%user_category/edit/new-tab']['page arguments'] = array('page argument 1', 'page argument 2', 'etc');
  // If your callback function is not in this file
  // $callbacks['user/%user_category/edit/new-tab']['file'] = 'mymodule.form.inc';
}

After this everything is working perfectly, here’s why. In user.module there is this block of code inside of user_menu

<?php
// in hook_menu
  if (($categories = _user_categories()) && (count($categories) > 1)) {
    foreach ($categories as $key => $category) {
      // 'account' is already handled by the MENU_DEFAULT_LOCAL_TASK.
      if ($category['name'] != 'account') {
        $items['user/%user_category/edit/' . $category['name']] = array(
          'title callback' => 'check_plain',
          'title arguments' => array($category['title']),
          'page callback' => 'drupal_get_form',
          'page arguments' => array('user_profile_form', 1, 3),
          'access callback' => isset($category['access callback']) ? $category['access callback'] : 'user_edit_access',
          'access arguments' => isset($category['access arguments']) ? $category['access arguments'] : array(1),
          'type' => MENU_LOCAL_TASK,
          'weight' => $category['weight'],
          'load arguments' => array('%map', '%index'),
          'tab_parent' => 'user/%/edit',
          'file' => 'user.pages.inc',
        );
      }
    }
  }

This takes all of the user_categories (hook_user_categories hint hint) and actually adds them all to the secondary level of tabs for you on the user edit form. Then you just need to override a few of the provided callbacks with your own(hook_menu_alter) and you’re done!