A few days ago Rob Sanchez tweeted a screenshot that caught my eye – a hook called cp_menu_array that is hanging out in expressionengine/libraries/Menu.php like it’s nothing special. It was added in EE 2.1.5.
Good thing Rob caught that because I can’t find any mention of it in the change log or the docs, and this thing is awesome. Why? We finally have direct control over the EE CP menu system.
A Brief Recent History of Messing with the CP Menu
First, a little overview of where we’ve been in recent history of the menu.
In EE2, menus on a bookmarks model. A common example is you’ve got an install of Structure and you want to just have a link to go to the structure area so you don’t have to go through the Add-ons -> Modules menu. You click the little +Add link on the top and you can add it as a custom-named link on the top.

This is great except that you’ll notice the controls for editing the name and order of these bookmarks are in your account section. IE, this bookmark is just for you, so that makes managing the experience for clients a lot harder. Make a new account for a client, and you need to add in these bookmarks if you want them to have that easy access.
If you wanted to extend the menu, you could use the cp_js_end hook and then used JS to add items to the menu using the DOM. This was fine, but messing around with adding/removing items with JS wasn’t an ideal solution. There is already an array with the items in there – just give us access!
The Keys to the … Array
So here comes cp_menu_array and basically EE is saying “here, take the entire menu array and have fun with it. Go nuts, really. Just give it back when you’re done.”
Awe. Some. This is that next level menu customization. Let’s have fun with it.
To play, you are going to need to use an extension. If you aren’t familiar with extensions, check out the EE docs on the matter. They are basically just functions that you can run at predetermined times in EE’s code.
Let’s look at the extension info in the activate_extension function:
public function activate_extension()
{
$this->settings = array();
$data = array(
'class' => __CLASS__,
'method' => 'build_custom_menu',
'hook' => 'cp_menu_array',
'settings' => serialize($this->settings),
'version' => $this->version,
'enabled' => 'y'
);
$this->EE->db->insert('extensions', $data);
}
Nothing surprising here. Just basically creating a regular extension, and we are saying we want to call the build_custom_menu function when EE gets around the to cp_menu_array hook.
Now, the fun part, our build_custom_menu function. This is actually really simple. The function just takes an array and expects you to give an array back. Here’s what a section looks like:
[content] => Array
(
[publish] => Array
(
[Pages] => index.php?S={string}&D=cp&C=content_publish
&M=entry_form&channel_id=2
[Schools] => index.php?S={string}&D=cp&C=content_publish
&M=entry_form&channel_id=1
)
[edit] => index.php?S={string}&D=cp&C=content_edit
[files] => Array
(
[file_manager] => index.php?S={string}&D=cp&C=content_files
[0] => ----
[file_upload_preferences] => index.php?S={string}&D=cp
&C=content_files&M=file_upload_preferences
[file_watermark_preferences] => index.php?S={string}&D=cp
&C=content_files&M=watermark_preferences
)
[0] => ----
[overview] => index.php?S={string}&D=cp&C=content
)
The root key of the array is the content menu item we are all familiar with, with the publish sub-menu item underneath it. Pretty straight forward. Each array item can be a string or array, and the array is another sub menu.
Example: Page Structure Link
Let’s try a simple example. We want to add a link under “Content” that says “Page Structure” and links to our Structure module. Here is our build_custom_menu function:
public function build_custom_menu($menu)
{
$menu['content']['pages'] = BASE.AMP.'C=addons_modules'.AMP.'M=
show_module_cp'.AMP.'module=structure';
return $menu;
}
We are simple adding our own item to the ‘content’ array inside of the $menu array and giving it a value of the link to the structure module.
Instead of naming the key “Page Structure” we need to keep than name in a language file and prefix it with ‘nav_’. So, inside language/english/custom_menu_lang.php we’d need this item in the $lang array:
'nav_pages' => 'Page Structure'
Now, we’ve got a sweet custom menu item!

Obeying Member Permissions
Okay so this looks awesome but remember that not everyone can access the Structure module so we shouldn’t tease them and show them what they could have if they were cool enough.
The solution to this is pretty simple: just do some checking for your particular menu item. Here is the user check from our example:
public function build_custom_menu($menu)
{
$assigned = $this->EE->session->userdata('assigned_modules');
if(
$this->EE->cp->allowed_group('can_access_modules') and
(
$this->EE->session->userdata('group_id') == 1 or
(isset($assigned[15]) and $assigned[15] == 'yes')
)
) {
$menu['content']['pages'] = BASE.AMP.'C=addons_modules'.AMP
.'M=show_module_cp'.AMP.'module=structure';
}
return $menu;
}
Above, we are checking that the user can access modules, and at they can access the Structure module. We need to check this by the module ID in the ‘assigned_modules’ userdata item., which kind of sucks because this can change from install to install, so its best to get the module ID by the name and use that.
Other Functions
To remove an item, just unset that part of the array:
unset($menu['content']['overview']);
To add a menu break, add an array value of four dashes (—-).
$menu['content'][] = '----';
Go Nuts!
This gives add-on developers and site developers a really fine grain of control over how menus are structured. Modules can also now add an extension that adds custom menu items. You can control your client experience down to a fine grain, and that’s good for everyone.
