Admin Menus in WordPress

The administration system in WordPress works well enough at first glance. The built-in menu functions take care of a lot of the issues such as adding and removing menu items, but you may want to learn more about how WordPress handles its menus internally to leverage more control. The example below will show how you can implement a plugin which enables you to reorder the menu items in the order you wish while explaining the nuances of the WordPress menu system.

How WordPress orders Menu Items

To best show how the position of a menu item can cause problems, let’s create a small plugin to demonstrate the way in which WordPress orders menu items when adding them.

Create a directory in the plugins directory, such as <root-installation>/wp-content/plugins/adminpagearranger. Then, create a file called init.php in the directory and use the WordPress plugin descriptor syntax to identify the plugin to the system.

The WordPress documentation advises that adding admin pages should be done with the admin_menu action hook, so add this directly under the descriptor.

The easiest way to add a menu item to the administration panel is with the add_menu_page() function which takes seven arguments:

  • text that is displayed in the title of the page
  • the text of the menu item which is used in displaying the menu item
  • the capability that you define to allow access to this menu item
  • a menu slug identifier which identifies this menu from any other in the system
  • a function which renders the page in the browser
  • the path to an icon which is used to display with the menu, and
  • the position that the new menu item should appear in relation to other menu items

Place the following code in init.php:

<?php
/*
Plugin Name: Admin Page Arranger
Plugin URI: http://www.example.com/adminpagearranger
Description: A plugin to adminster admin pages
Version: 0.1
Author: Tim Smith
Author URI: http://www.example.com/
License: GPL2
*/

add_action("admin_menu", "addMenu");

function addMenu() {
    add_menu_page("My New Menu", "My New Menu", "edit_posts",
        "mynewmenu", "displayPage", null, 1);
}

The code will generate a new page link which sits at the top of the menu structure, just above the Dashboard:

But, consider the following implementation of addMenu() instead:

<?php
function addMenu() {
    add_menu_page("My New Menu", "My New Menu", "edit_posts",
        "mynewmenu", "displayPage", null, 2);
}

Just by changing the position from 1 to 2 replaces the Dashboard entry with your new menu! Why? It’s all got to do with how WordPress stores its menu configuration.

WordPress uses a global array called $menu to store the menu configuration, and each key in the array represents the position in the menu. A higher position in the array indicates a higher position in the menu. However, WordPress populates this array on start up with default values. For example:

Array
(
    [2] => Array
        (
            [0] => Dashboard
            [1] => read
            [2] => index.php
            [3] => 
            [4] => menu-top menu-top-first menu-icon-dashboard
            [5] => menu-dashboard
            [6] => div
        )

    [4] => Array
        (
            [0] => 
            [1] => read
            [2] => separator1
            [3] => 
            [4] => wp-menu-separator
        )

    [5] => Array
        (
            [0] => Posts
            [1] => edit_posts
            [2] => edit.php
            [3] => 
            [4] => open-if-no-js menu-top menu-icon-post
            [5] => menu-posts
            [6] => div
        )

    [10] => Array
        (
            [0] => Media
            [1] => upload_files
            [2] => upload.php
            [3] => 
            [4] => menu-top menu-icon-media
            [5] => menu-media
            [6] => div
        )
...
)

Each entry in the array is in fact another array providing the details of each menu item. So when the call to add a menu item at position 2 is executed, it actually replaces whatever may have been at position 2 prior. In this case, this is the dashboard’s information.

When you’re adding a new menu item, be mindful of the position you specify. Note also that the arguments supplied to the add_menu_page() function have now been used to populate the global array.

Specifying a Custom Menu Order

Now that you know how WordPress stores its menu configuration, you can use this to control where menu items appear in the $menu array and thus change the order in which the menus appear. WordPress offers two filters which can be used to change the menu: menu_order and custom_menu_order.

custom_menu_order requires a Boolean value to be returned which indicates whether a custom menu should be used. In effect, the returned value from custom_menu_order dictates whether the menu_order filter is applied.

The return value of the custom_menu_order filter should return an array detailing the slugs of the menus you want to appear in their order. You can use the following table to select the slugs which relate to the standard menu items.

<?php
add_filter("custom_menu_order", "allowMenuStructure");
add_filter("menu_order", "loadMenuStructure");

function allowMenuStructure() {
    return true;
}

function loadMenuStructure() {
    return array("index.php", "tools.php");
}

You don’t have to specify all of the slugs to be displayed when returning the array from custom_menu_order; WordPress will automatically populate the rest of the menu with the remaining menu items in the order they would normally be placed.

Creating a Custom Menu Manager

We can leverage the custom_menu_order filter to populate the menu in the desired order by storing an array in WordPress to be returned when this filter is applied. You do this by way of update_option which either creates or updates a value which you can call later with get_option(). WordPress stores the data in the database table wp_options for later retrieval. If the option has not yet been sent, or is absent for whatever reason, you can create it based on the default menu structure.

The first thing you should do is add your own menu item so that you can administer the menu order. The returned value from add_menu_page() is a “hook suffix” which you can append to a number of action hooks which are specific to the page. The one we are interested in here is load-page_hook where page_hook should be substituted with the hook suffix.

This hook is an ideal place to process form submissions as it will only occur when this page is loaded.

Change the addMenu() function we used earlier to test the menu functionality as follows:

<?php
function addMenu() {
    $page = add_menu_page("Admin Manager", "Admin Manager",
        "manage_options", "adminmanager", "displayAdminManager",
        null, null);
    add_action("load-" . $page, "handleMenu");
}

This short piece of code obliges us define two more functions – displayAdminManager() which is responsible for displaying the page and handleMenu() which we will use to process any changes we make to the menu order.

Before you define these, however, it makes sense to define a function which loads a menu structure obtained from the wp_options table or sets up a default. This function will be used in a couple of places.

<?php
function getMenuStructure() {
    $structure = get_option("menustructure");
    if (!$structure) {
        global $menu;
        $newMenu = array();
        foreach ($menu as $menuItem) {
            $newMenu[] = $menuItem[2];
        }
        return $newMenu;
    }
    else {
        return $structure;
    }
}

getMenuStructure() uses get_option() to attempt to pull a menu structure from the database. If it isn’t available, then it constructs an array to pass back. This function will be used to actually populate your new menu structure and also to list the menu items when displaying the plugin so you can move the items up and down the structure.

You may find that some plugin developers place their own menu items wherever they want. This can sometimes be inconvenient as you will find that the normal menu items will be separated from where they should be. Plugin developers are dissuaded from doing this in general, but you’ll find occasionally someone feels their plugin is more important than the others and so they add it to the top of the menu system. If you want to get around this and make sure that all other additional menu items are placed below everything else, you can change the function to:

<?php
function getMenuStructure() {
    $structure = get_option("menustructure");
    if (!$structure) {
        return array("index.php", "separator1", "edit.php",
            "upload.php", "link-manager.php",
            "edit.php?post_type=page",
            "edit-comments.php", "separator2", "themes.php",
            "plugins.php", "users.php", "tools.php",
            "options-general.php", "separator-last");
    }
    else {
        return $structure;
    }
}

This enforces that the default WordPress menu structure is implemented and any other items will be displayed after it.

In the hard-coded structure, noticed I also used separator entries. The separators should be identified uniquely with the word “separator” followed by an integer, a the last one should be identified as “separator-last”.

So now you have a generic function for obtaining a menu structure and you can look at defining the function to display the menu manager so that the menu can be manipulated. You’ll need to include links so that when clicked, they reload the page whereupon the load-page_hook action is run.

<?php
function displayAdminManager() {
    $menu = getMenuStructure();
    foreach($menu as $key => $menuitem) {
        $uplink = "admin.php?page=adminmanager&menuitem=" . $key . "&direction=up";
        $downlink = "admin.php?page=adminmanager&menuitem=" . $key . "&direction=down";
        echo $menuitem . ' - <a href="' . $uplink . '">Up</a> <a href="' . $downlink . '">Down</a><br>';
    }
}

Note that we call the page itself; the URL for this page is /admin.php?page=adminmanager. The adminmanager identifier is the slug that was set when you added the page via add_menu_page() which was the fourth argument. There are also a couple of arguments detailing the direction the menu item should be moved (i.e. up or down) and which menu item will be affected.

So, now you have a means by which we can pass arguments to process via load-page_hook which was defined as handleMenu(). The key to this is that we swap the menu items over depending on the direction that was specified and when this is done, you use update_option() to store your new menu structure. Then the viewer is redirected back to “this” page. This ensures the new menu is loaded and implemented.

It is also a good idea to ensure that menu items cannot be moved off the structure by testing whether the menu item to be manipulated is the first or last in the array.

<?php
function handleMenu() {

    if (isset($_GET["menuitem"]) && isset($_GET["direction"])) {
        $structure = get_option("menustructure");
        $numberItems = count($structure);
        $lastIndex = $numberItems - 1;

        if (!$structure) {
            $menu = getMenuStructure();
        }
        else {
            $menu = $structure;
        }

        // if the menu item is to be moved UP the menu
        if ($_GET["direction"] == "down" && $_GET["menuitem"] != $lastIndex) {
            // move the menu up
            $currentIndex = $_GET["menuitem"];
            $nextIndex = $currentIndex + 1;

            $currentMenuItem = $menu[$currentIndex];
            $nextMenuItem = $menu[$nextIndex];

            $menu[$currentIndex] = $nextMenuItem;
            $menu[$nextIndex] = $currentMenuItem;

            update_option("menustructure", $menu);
        }
        //if the menu item is to be moved DOWN the menu
        elseif ($_GET["direction"] == "up" && $_GET["menuitem"] != 0)
        {
            //move the menu down
            $currentIndex = $_GET["menuitem"];
            $previousIndex = $currentIndex - 1;

            $currentMenuItem = $menu[$currentIndex];
            $previousMenuItem = $menu[$previousIndex];

            $menu[$currentIndex] = $previousMenuItem;
            $menu[$previousIndex] = $currentMenuItem;

            update_option("menustructure", $menu);
        }

        wp_redirect("admin.php?page=adminmanager");
    }
}

Redirection is necessary because of the order in which the actions take place. The load-page_hook action occurs after the custom_menu_order filter, so updating the “menustructure” option takes place but you won’t see the effect since the custom menu order has already been loaded. A redirect will ensure the new menu structure is implemented correctly by essentially “refreshing” the page and the WordPress life cycle.

Summary

This tutorial taught you how the internal admin menus are structured in WordPress. You built a simple plugin to implement a system whereby you can change the order of the menu items. We used WordPress’s internal options to store and retrieve this data and a custom filter to ensure our custom menu order is implemented.

James Threw / Shutterstock

Win an Annual Membership to Learnable,

SitePoint's Learning Platform

  • http://www.mStudios.com Marlyse Comte

    Nice tutorial – with the exception that the menu order does not update. Within the Admin Menu page it goes up and down in the list, but the true order of the menu does not update, even though the redirect is in place. I am using version 3.4 of WP. Am I missing something maybe? Thanks!