DQX AdminMenu: Dev notes

Technical notes about the DQX AdminMenu module, that were too long for the module page. Warning: long read!

For a long time, Administration Menu module has been the first thing I would install on any site I build.

Until I started to be unhappy about a few things that I missed in both the 1.x and the 3.x branch. The missing "Create content" link in the 3.x branch, faulty menu reparenting in Drupal core, the difficulty of rendering a menu or submenu as anything but a nested list of items.

Trying to help myself with a custom "admin_menu_fix" module, I realized that some of the problems and limitations were caused directly by the way the module interacts with the Drupal menu system.

Meet DQX AdminMenu

Finally I decided to craft a replacement for admin_menu, keeping the beloved appearance and menu structure, but based upon a technical approach that is radically different from the original.

The new module is now published on drupal.org as DQX AdminMenu.

The new project is meant as a 99% alternative to the "original", with "mega dropdowns" optimized for fast access to anything related to content types, and with none of the problems that I had with the old one.

There have been quite long explanations on the module page about the motivation for this project, but I think these are far better off in this blog post.

Overview of this article

This article attempts to answer these technical questions:

  1. How does the original Admin Menu work, technically? What are the differences of the 1.x and 3.x branch?
  2. How does the new DQX AdminMenu work, and how is this (technically) different from the "original" Admin Menu?
  3. Why did I start a new project, instead of proposing the changes as a patch to the original Admin Menu?

If you want to know how to use the module, and what it can do for you, I can only recommend to skip this article and check out the module page instead.

The Drupal menu system (D6/D7)

Drupal's menu system works with two database tables: {menu_router} and {menu_links}.

{menu_router}

Items in the {menu_router} table are collected from hook_menu(), and contain information about page callbacks, access callbacks, wildcard loaders, etc. Anything that is needed so Drupal can decide which content to show for a given url path. In this aspect, {menu_router} has very little to do with what anyone would expect by the term "menu".

In addition to this basic routing information, every item in menu_router does also have some meta info, such as, a title, and a flags array, saying something like "MENU_LOCAL_TASK", or "MENU_NORMAL_ITEM". This information can be regarded as "hints" for generating a displayable menu structure, tabs ("local tasks") and breadcrumbs. The link path itself is used as a hint for the hierarchy of items: Chop off the last path fragment, and you get the parent path.

On a menu_rebuild(), Drupal will invoke all the implementations of hook_menu(), to update / repopulate the menu_router table.

{menu_links}

The {menu_links} table is something different: Here we find displayable menu items, configured by a site administrator via the admin backend. If you don't know it already, Menu Editor is your friend.

For each menu link we store a link path, a link title, a parent item (unless it is a "root" item), and the name (machine key) of the menu it belongs to.

Whenever a menu link is saved, it will be associated with a router item, to make sure that it is a valid link path, and to let the router system do the access checking. Other than that, there is little the menu links have in common with menu router.

There are a few menus that exist by default, or created by modules, such as "Primary links". Any other menu has to be created manually via the admin backend. Usually these menus start empty, until you fill them with links.

The "bad kid in town": System Navigation

Unlike the other menus, "navigation" starts pre-populated with menu items that are generated from the {menu_router} table. Whenever the router table is rebuilt, Drupal will update the links table reflecting these changes. Drupal uses the meta info in the router items (flags for "MENU_DEFAULT_ITEM", link title, etc), to determine the link title, the parent item, etc.

At the same time, Drupal allows the user to manipulate the items in the navigation menu via the admin backend. Each modified item will be tagged as "customized", to protect it on the next rebuild operation.

This entire rebuild operation (_menu_navigation_links_rebuild()) is as slow as it is scary, and only few people know what is actually going on.

As a result, the navigation menu contains a menu tree for (among other things) the complete admin backend, content creation, and dealing with your user profile. Which is basically a good thing.

The entire menu is actually so big and "complete" that it is a nightmare to visit the form to edit its items. As a site builder, you can only hope that the client will never visit this page, forcing you to do the same to clean it up.

As I see it, the possibility to customize that particular menu brings very little benefit, and causes nothing but confusion, if some items are no longer in the place where they are expected to be. If your site needs a customized menu (which it does, in most cases), use the primary links, or create your own.

Administration Menu - "the original"

If a maintainer of admin_menu reads this and finds a mistake, please comment and let me know!

The "Admin menu" module uses the structure of the system navigation menu, making a big part of it available as a huge dropdown.

The 6.x-1.x branch had a strategy of duplicating the entire structure into a separate menu with a machine key of "admin_menu". This means, the "navigation" menu was still intact with all items.

Other modules had a chance to hook into that copying process, and manipulate the structure. In this phase, they were still dealing with a flat array with string keys from menu_router, and did not have to deal with customized links - these would be dealt with at a later step.

The 6.x-3.x branch does instead "steal" all the admin-related items from the "navigation" menu, and declare them as belonging to "admin_menu" instead of "navigation".

The hook to manipulate the router structure was kicked, and now contrib modules have to deal with auto-increment ids, customized items, and deep nested arrays, if they want to alter the structure.

In any case, the module will use hook_menu_alter() to trigger this operation at the end of every menu rebuild.

Same as for system nav, the user now has the possibility to customize this menu. And same as for system nav, this act of customization is usually quite impractical.

As a consequence, anything that goes wrong with system nav rebuild, will also affect admin_menu. Which is one reason why we the 3.x was in alpha4 for a long time, and is still (Feb 19, 2011) tagged as "rc1".

DQX: Goodbye {menu_links}

The DQX AdminMenu can avoid all this pain by simply not using the menu_links table at all.

Instead, every time the structure has to be rebuilt, it will look into the {menu_router} table and build a hierarchy from that. Instead of storing this in menu_links, it will simply just cache the tree in a serialized array, and render it.

Goodbye "customized"

There is no possibility to customize these links manually - but why would you? Instead, it becomes much easier to customize the links programmatically, because we don't need to respect any manual changes. All hardcoded, atm, but technically this is all very suitable for an API.

Goodbye auto-increment

Skipping menu_links and manual customization made it possible to have string keys for all menu items, instead of auto-increment mlids. In most cases, but not always, these are identical with the link path. Again, this is great for programmatic customization, and there is zero to do in migration/deployment.

Goodbye deep nested arrays

In the core menu system, the way to access a deeply nested menu item was $tree[$grandparent]['below'][$parent]['below'][$mlid]. The new way is just $items[$string_key]. And to get the children, you say $submenus[$mlid]. Not having to dive into nested arrays is great for programmatic customization. And also makes the initial tree generation algorithm much simpler.

Client-side goodness

Client-side cache, the simple way

I still don't know how exactly admin_menu's client-side cache works. It looked complex to me, but it's probably cool.

Fortunately, I found a very cheap alternative: Have the tree structure stored in a big javascript file. What can the browser do best? Cache javascript files.

The file is not actually stored on the server as a file, but served by php.

Whenever that file needs to be updated, we simply give it a new GET suffix. Done.

Of course this will only work with js enabled. But so be it.

Is this better than what admin_menu does? No idea, but it works.

API?

There is no API, yet. But we will get there, eventually.

Conclusion

I think it has become clear why these changes had to go into a new module. I did not want to push the admin_menu maintainers to a design decision as radical as this, and I wanted a testing ground that does not have 160K users.

I like a lot of the features of the original, and the appearance, so I thought I leave that unchanged.

I don't want to name a module "adminmenu", when there is another one around called "admin_menu". This would only cause confusion. Thus, I use the dqx = donquixote as a prefix. Ok?

Add new comment