Save menu links programatically in Drupal 7

This post is part of a series of posts on making changes in Drupal programmatically rather than in the Drupal interface.

In this tutorial, we will look at saving menu items programmatically. This can be done in the .install file of a site deployment module in order to automate this task rather than having to manually assign the roles in the Drupal UI.

Standard menu item

Standard menu items are relatively straight forward. You build up an array with the title of the link, path and the menu you want to save the item under (main-menu in this case). If the menu item doesn't have a parent, the plid (parent link ID) is 0. You then save the menu item using menu_link_save();

  1. function site_deployment_update_7000() {
  2. $item = array (
  3. 'link_title' => 'Example Link Title',
  4. 'link_path' => 'example/path',
  5. 'menu_name' => 'main-menu',
  6. 'weight' => 0,
  7. 'plid' => 0,
  8. );
  9. menu_link_save($item);
  10. }

Node path

Saving a menu item for a node is a bit more complicated, especially if you know the node alias but not the node path. The node path is the one with the node ID (e.g. node/33). The menu system doesn't want to save the path alias, it wants the real node path. So in this case, you can use drupal_lookup_path() to get the path from the alias. The first argument for drupal_lookup_path() is source, which will ensure it returns the node system URL. And then use that as the link_path. The router_path is simply node/% whenever you are saving a menu item for a node.

When saving node paths, it is important to note that the node must exist and be published for it to appear in the actual menu.

  1. function site_deployment_update_7001() {
  2. $path = drupal_lookup_path('source', 'example-node-alias');
  3. $item = array (
  4. 'link_title' => 'Example Link Title',
  5. 'link_path' => $path,
  6. 'router_path' => 'node/%',
  7. 'menu_name' => 'main-menu',
  8. 'weight' => 0,
  9. 'plid' => 0,
  10. );
  11. menu_link_save($item);
  12. }

Parent and child menu items

Things get trickier if you want to save a parent menu item and child menu items at the same time.

The parent menu item is much the same, except that you store the result of menu_link_save in a variable ($plid). This will be the ID of the menu item, which you can use in the parent link ID (plid) in the child menu items.

  1. $path = drupal_lookup_path('source', 'example-node-alias');
  2. $item = array (
  3. 'link_title' => 'Example Link Title',
  4. 'link_path' => $path,
  5. 'router_path' => 'node/%',
  6. 'menu_name' => 'main-menu',
  7. 'weight' => 0,
  8. 'plid' => 0,
  9. );
  11. $plid = menu_link_save($item);

Here is the first child menu item, using the parent ID ($plid) from the menu link just saved:

  1. $item = array (
  2. 'link_title' => 'Example2 Link Title',
  3. 'link_path' => 'path/to/example2',
  4. 'menu_name' => 'main-menu',
  5. 'weight' => 0,
  6. 'plid' => $plid,
  7. );

Programmatically saving a menu item

Here is the full example, where we are saving the parent menu item and two children.

  1. function site_deployment_update_7002() {
  2. $path = drupal_lookup_path('source', 'example-node-alias');
  3. $item = array (
  4. 'link_title' => 'Example Link Title',
  5. 'link_path' => $path,
  6. 'router_path' => 'node/%',
  7. 'menu_name' => 'main-menu',
  8. 'weight' => 0,
  9. 'plid' => 0,
  10. );
  12. $mlid = menu_link_save($item);
  14. $item = array (
  15. 'link_title' => 'Example2 Link Title',
  16. 'link_path' => 'path/to/example2',
  17. 'menu_name' => 'main-menu',
  18. 'weight' => 0,
  19. 'plid' => $plid,
  20. );
  22. menu_link_save($item);
  24. $item = array (
  25. 'link_title' => 'Example3 Link Title',
  26. 'link_path' => 'path/to/example3',
  27. 'menu_name' => 'main-menu',
  28. 'weight' => 0,
  29. 'plid' => $plid,
  30. );
  32. menu_link_save($item);
  33. }

Hide the menu item

You want to save a new menu item but immediately disable it. This isn't as crazy as it sounds. I have done this recently on a client project. The link is live on the staging site, but the client wants it to exist on live but disabled for now until more nodes are created. They can simple enable it when ready.

To do this, add the hide property and give it a value of 1. 1 means hide (disable), 0 means do not hide (enable).

  1. function site_deployment_update_7003() {
  2. $item = array (
  3. 'link_title' => 'Example Link Title',
  4. 'link_path' => 'path/to/example',
  5. 'menu_name' => 'main-menu',
  6. 'weight' => 0,
  7. 'plid' => $plid,
  8. 'hide' => 1,
  9. );
  10. menu_link_save($item);
  11. }


Very nice tutorial, I've followed the same logic to nest existing node/add/existingcontenttype menu items under new parents I've created programmatically. It works but it creates another problem, some of the previously existing node/add/existingcontenttype items are not appearing anymore on node/add page. Any suggestions on how to solve this problem? There are some suggestions online about using theme_node_add_list() hook but they're not very clear, I'd appreciate some input here, thank you.

I'm new developing in drupal, I understand the logic of this example but I don't understand when I should call these functions, am I supposed to have them in a .inc file? or call them thru a hook function?

Blair Wadman's picture

Hi Juan, these functions should go into the .install file of a custom module. I created a detailed tutorial on this in another tutorial, which you might find helpful:

Let me know if that clears it up. Otherwise, I'll try and provide more details.

Thanks for the nice write up. Just noticed a typo. I believe 'hide' should be 'hidden' to disable menu items.

  1. $item = array (
  2. 'hidden' => 1,
  3. );
  4. menu_link_save($item);


New comments for this tutorial have been turned off.