With the help of the wp_get_nav_menu_items filter, you can easily add dynamic links to a WordPress menu without having to concatenate html strings, or write a custom nav walker.

This example shows how to use the filter and construct a pseudo menu-item object (to meet the expectations of a nav walker class) to add a dynamic “My Profile” link to an existing menu.

The first thing we need is a simple helper function that creates a menu item object according to WordPress’ expectations.

/**
 * Simple helper function for make menu item objects
 * 
 * @param $title      - menu item title
 * @param $url        - menu item url
 * @param $order      - where the item should appear in the menu
 * @param int $parent - the item's parent item
 * @return \stdClass
 */ 
function _custom_nav_menu_item( $title, $url, $order, $parent = 0 ){
  $item = new stdClass();
  $item->ID = 1000000 + $order + parent;
  $item->db_id = $item->ID;
  $item->title = $title;
  $item->url = $url;
  $item->menu_order = $order;
  $item->menu_item_parent = $parent;
  $item->type = '';
  $item->object = '';
  $item->object_id = '';
  $item->classes = array();
  $item->target = '';
  $item->attr_title = '';
  $item->description = '';
  $item->xfn = '';
  $item->status = '';
  return $item;
}

To use this helper function, you need to take advantage of the wp_get_nav_menu_items filter so we can add menu items as desired:

add_filter( 'wp_get_nav_menu_items', 'custom_nav_menu_items', 20, 2 );

function custom_nav_menu_items( $items, $menu ){
  // only add item to a specific menu
  if ( $menu->slug == 'menu-1' ){
    
    // only add profile link if user is logged in
    if ( get_current_user_id() ){
      $items[] = _custom_nav_menu_item( 'My Profile', get_author_posts_url( get_current_user_id() ), 3 ); 
    }
  }
    
  return $items;
}

That’s it! To create a hierarchy (dropdown) of menu items, you could write your hook like this:

add_filter( 'wp_get_nav_menu_items', 'custom_nav_menu_items2', 20, 2 );

function custom_nav_menu_items2( $items, $menu ) {
  if ( $menu->slug == 'menu-1' ) {
    $top = _custom_nav_menu_item( 'Top level', '/some-url', 100 );

    $items[] = $top;
    $items[] = _custom_nav_menu_item( 'First Child', '/some-url', 101, $top->ID );
    $items[] = _custom_nav_menu_item( 'Third Child', '/some-url', 103, $top->ID );
    $items[] = _custom_nav_menu_item( 'Second Child', '/some-url', 102, $top->ID );
  }

  return $items;
}

Be careful. It’s important that no menu items share the same menu_order property.

Finally, I’ve created a class and example of its usage as a gist: WordPress class to add custom menu items dynamically.  See the first comment on the gist for usage examples.

<?php

class custom_menu_items {
	// only register with wp hooks once
	protected $has_registered = false;

	// internal list of menus affected
	public $menus = array();

	// internal list of new menu items
	public $menu_items = array();

	private function __construct(){}
	private function __wakeup() {}
	private function __clone() {}

	/**
	 * Singleton
	 *
	 * @return custom_menu_items
	 */
	static public function get_instance(){
		static $instance = null;

		if ( is_null( $instance ) ){
			$instance = new self;
		}

		$instance->register();
		return $instance;
	}

	/**
	 * Hook up plugin with WP
	 */
	private function register(){
		if ( ! is_admin() && ! $this->has_registered ){
			$this->has_registered = true;

			add_filter( 'wp_get_nav_menu_items', array( $this, 'wp_get_nav_menu_items' ), 20, 2 );
			add_filter( 'wp_get_nav_menu_object', array( $this, 'wp_get_nav_menu_object' ), 20, 2 );
		}
	}

	/**
	 * Update the menu items count when building the menu
	 *
	 * @param $menu_obj
	 * @param $menu
	 *
	 * @return mixed
	 */
	function wp_get_nav_menu_object( $menu_obj, $menu ){
		if ( is_a( $menu_obj, 'WP_Term' ) && isset( $this->menus[ $menu_obj->slug ] ) ){
			$menu_obj->count += $this->count_menu_items( $menu_obj->slug );
		}
		return $menu_obj;
	}

	/**
	 * Get the menu items from WP and add our new ones
	 *
	 * @param $items
	 * @param $menu
	 *
	 * @return mixed
	 */
	function wp_get_nav_menu_items( $items, $menu ){
		if ( isset( $this->menus[ $menu->slug ] ) ) {
			$new_items = $this->get_menu_items( $menu->slug );

			if ( ! empty( $new_items ) ) {
				foreach ( $new_items as $new_item ) {
					$items[] = $this->make_item_obj( $new_item );
				}
			}

			$items = $this->fix_menu_orders( $items );
		}

		return $items;
	}

	/**
	 * Entry point.
	 * Add a new menu item to the list of custom menu items
	 *
	 * @param $menu_slug
	 * @param $title
	 * @param $url
	 * @param $order
	 * @param $parent
	 * @param null $ID
	 */
	static public function add_item( $menu_slug, $title, $url, $order = 0, $parent = 0, $ID = null ){
		$instance = custom_menu_items::get_instance();
		$instance->menus[ $menu_slug ] = $menu_slug;
		$instance->menu_items[] = array(
			'menu'   => $menu_slug,
			'title'  => $title,
			'url'    => $url,
			'order'  => $order,
			'parent' => $parent,
			'ID'     => $ID,
		);
	}

	/**
	 * Add a WP_Post or WP_Term to the menu using the object ID.
	 *
	 * @param $menu_slug
	 * @param $object_ID
	 * @param string $object_type
	 * @param $order
	 * @param $parent
	 * @param null $ID
	 */
	static public function add_object( $menu_slug, $object_ID, $object_type = 'post', $order = 0, $parent = 0, $ID = NULL ) {
		$instance = custom_menu_items::get_instance();
		$instance->menus[ $menu_slug ] = $menu_slug;

		if ($object_type == 'post' && $object = get_post( $object_ID ) ) {
			$instance->menu_items[] = array(
				'menu'   => $menu_slug,
				'order'  => $order,
				'parent' => $parent,
				'title'  => get_the_title($object),
				'url'    => get_permalink($object),
				'ID'     => $ID,
				'type'   => 'post_type',
				'object' => get_post_type($object),
				'object_id' => $object_ID,
			);
		}
		else if ($object_type == 'term') {
			global $wpdb;
			$sql = "SELECT t.*,tt.taxonomy FROM {$wpdb->terms} as t LEFT JOIN {$wpdb->term_taxonomy} as tt on tt.term_id = t.term_id WHERE t.term_id = %d";
			$object = $wpdb->get_row($wpdb->prepare($sql, $object_ID));

			if ( $object ) {
				$instance->menu_items[] = $tmp = array(
					'menu'   => $menu_slug,
					'order'  => $order,
					'parent' => $parent,
					'title'  => $object->name,
					'url'    => get_term_link((int)$object->term_id, $object->taxonomy),
					'ID'     => $ID,
					'type'   => 'taxonomy',
					'object' => $object->taxonomy,
					'object_id' => $object_ID,
				);
			}
		}
	}

	/**
	 * Get an array of new menu items for a specific menu slug
	 *
	 * @param $menu_slug
	 *
	 * @return array
	 */
	private function get_menu_items( $menu_slug ){
		$items = array();

		if ( isset( $this->menus[ $menu_slug ] ) ) {
			$items = array_filter( $this->menu_items, function ( $item ) use ( $menu_slug ) {
				return $item['menu'] == $menu_slug;
			} );
		}
		return $items;
	}

	/**
	 * Count the number of new menu items we are adding to an individual menu
	 *
	 * @param $menu_slug
	 *
	 * @return int
	 */
	private function count_menu_items( $menu_slug ){
		if ( ! isset( $this->menus[ $menu_slug ] ) ) {
			return 0;
		}

		$items = $this->get_menu_items( $menu_slug );

		return count( $items );
	}

	/**
	 * Helper to create item IDs
	 *
	 * @param $item
	 *
	 * @return int
	 */
	private function make_item_ID( $item ){
		return 1000000 + $item['order'] + $item['parent'];
	}

	/**
	 * Make a stored item array into a menu item object
	 *
	 * @param array $item
	 *
	 * @return mixed
	 */
	private function make_item_obj( $item ) {
		// generic object made to look like a post object
		$item_obj                   = new stdClass();
		$item_obj->ID               = ( $item['ID'] ) ? $item['ID'] : $this->make_item_ID( $item );
		$item_obj->title            = $item['title'];
		$item_obj->url              = $item['url'];
		$item_obj->menu_order       = $item['order'];
		$item_obj->menu_item_parent = $item['parent'];

		// menu specific properties
		$item_obj->db_id            = $item_obj->ID;
		$item_obj->type             = !empty( $item['type'] ) ? $item['type'] : '';
		$item_obj->object           = !empty( $item['object'] ) ? $item['object'] : '';
		$item_obj->object_id        = !empty( $item['object_id'] ) ? $item['object_id'] : '';

		// output attributes
		$item_obj->classes          = array();
		$item_obj->target           = '';
		$item_obj->attr_title       = '';
		$item_obj->description      = '';
		$item_obj->xfn              = '';
		$item_obj->status           = '';

		return $item_obj;
	}

	/**
	 * Menu items with the same menu_order property cause a conflict. This
	 * method attempts to provide each menu item with its own unique order value.
	 * Thanks @codepuncher
	 *
	 * @param $items
	 *
	 * @return mixed
	 */
	private function fix_menu_orders( $items ){
		$items = wp_list_sort( $items, 'menu_order' );

		for( $i = 0; $i < count( $items ); $i++ ){
			$items[ $i ]->menu_order = $i;
		}

		return $items;
	}
}

 

About the Author

Jonathan Daggerhart

Long time Drupal and WordPress developer. I like to write modules and plugins, and I dabble in frontend and design.

14 thoughts on “Dynamically add items to WordPress menus

  • Janos Ver

    Thanks for this article. I have found it very useful. The only thing I’d like to add is that I discovered it today that once I added the above code to my site to replace a menu item based on user action broke the Appearance-> Customize menu and I got an error (Exception) saying some properties are missing. By looking at the object definition at https://developer.wordpress.org/reference/functions/wp_setup_nav_menu_item/ and the error message itself I figured out that the following were missing from _custom_nav_menu_item function (header and return just added to provide some context):

    function _custom_nav_menu_item( $title, $url, $order, $parent = 0 ){

    $item->target = ”;
    $item->attr_title = ”;
    $item->description = ”;
    $item->xfn = ”;
    $item->status = ”;


    return $item;
    }

    I hope it saves some time and headache for others looking to use this filter.

  • Dave Spencer

    I too found this useful. Care to expand it to so as to be able to create a menu with submenus? I would like to be able to inject an item in to the menu bar that has a dropdown with submenus below it. Just a thought – it would make this the ultimate guide!

    • Jonathan Daggerhart

      Hi Dave,

      I’ve updated the post to show how to create submenu items. Note: I had to make a small update to the “_custom_nav_menu_item” function (db_id) for this to work.

      Let me know how it goes!

  • alan

    Thanks for this snippet, just an small issue in your helper

    $item->menu_parent_item = $parent;

    should be

    $item->menu_item_parent = $parent;

  • Cédric

    Thank you very much for your article.
    I create some pages dynamically using a PhP file. Adding your code at the beginning of the file, the page is correctly inserted in the menu but it’s not highlighted in menu and breadcrumb is not correct?
    Any idea ?

  • Litbea

    Jonathan, thanks a lot for this information.

    I’m using your article to try and SUCCESSFULLY create sub-menu items with this changes and specifying an already created parent item:

    function _custom_nav_menu_item( $title, $url, $order, $parent = 0 ){
    $item = new stdClass();
    $item->ID = 1000000 + $order + parent;
    $item->db_id = $item->ID;
    $item->title = $title;
    $item->url = $url;
    $item->menu_order = $order;
    $item->menu_item_parent = PARENT_MENU_ITEM_ID;//$parent;
    $item->type = ”;
    $item->object = ”;
    $item->object_id = ”;
    $item->classes = array();
    $item->target = ”;
    $item->attr_title = ”;
    $item->description = ”;
    $item->xfn = ”;
    $item->status = ”;
    return $item;
    }

    add_filter( ‘wp_get_nav_menu_items’, ‘custom_nav_menu_items2’, 20, 2 );

    function custom_nav_menu_items2( $items, $menu ) {
    if ( $menu->slug == ‘menu-1’ ) {
    //$top = _custom_nav_menu_item( ‘Top level’, ‘/some-url’, 100 );

    //$items[] = $top;
    //$items[] = _custom_nav_menu_item( ‘First Child’, ‘/some-url’, 101, $top->ID );
    //$items[] = _custom_nav_menu_item( ‘Third Child’, ‘/some-url’, 103, $top->ID );
    //$items[] = _custom_nav_menu_item( ‘Second Child’, ‘/some-url’, 102, $top->ID );
    // the query
    $wpb_all_query = new WP_Query(array(‘post_type’=>’custom_post_type_name’, ‘post_status’=>’publish’, ‘posts_per_page’=>-1, ‘orderby’ => ‘title’, ‘order’ => ‘ASC’));
    if ( $wpb_all_query->have_posts() ) :
    $subposition = 201;
    while ( $wpb_all_query->have_posts() ) : $wpb_all_query->the_post();
    // get the custom post data
    $title = get_the_title();
    $slug = basename(get_permalink());
    // create the menu sub item
    $items[] = _custom_nav_menu_item( $title, ‘/’.$slug, $subposition++, $top->ID );
    endwhile;
    wp_reset_postdata();
    else :
    echo ”. _e( ‘Sorry, no posts matched your criteria.’ ).”;
    endif;
    }

    return $items;
    }

    Working great!

  • Litbea

    Hi again, the only problem I’ve found is that a “The given object ID is not that of a menu item.” error shows up when saving the menu from /wp-admin/nav-menus.php

    Any suggestion?

  • Pingback: Populating main menu from external JSON API
  • Lorenzo

    Hello,
    I’m trying to use your class in a plugin i’m working on but i got some errors.
    here’s the relevant code samples:
    inside my main plugin file:

    add_action('wp_loaded', function () {
    ...
    //that's the method that runs our code inside:
    MenuWP::createWPMenuItems();
    });
    

    here’s the static function definition:

    public static function creaWPMenuItems()
        {
            $menuTree = ['node'=>['parentMenu'=>'menu-1',
                                  'text'=>'text menu item 1',
                                  'url'=>'http://www.google.com',
                                  'id'=>'ext_link']];
          foreach ($menuTree as $menuNode) {
              custom_menu_items::add_item($menuNode['parentMenu'], $menuNode['text'], $menuNode['url'], $menuNode['id']);
          }
        }
    

    the item gets created and correctly displayed but then, whene i try to edit the menu from wp-admin/nav-menus.php i got the following errors and edits do not get saved:

    E_NOTICE  Undefined property: stdClass::$post_excerpt - /vagrant/wp_clean/wp-content/themes/Avada/includes/class-avada-nav-walker-megamenu.php:166 
    #5 /vagrant/wp_clean/wp-includes/class-wp-walker.php:146 - Avada_Nav_Walker_Megamenu->start_el('			display_element(stdClass, Array[0], 0, 0, Array[1], '			walk(Array[5], 0, stdClass)
    #2 /vagrant/wp_clean/wp-admin/includes/nav-menu.php:958 - walk_nav_menu_tree(Array[5], 0, stdClass)
    #1 /vagrant/wp_clean/wp-admin/nav-menus.php:496 - wp_get_nav_menu_to_edit(3)
    

    Do you have any suggestions?

    thanks!

    • Jonathan Daggerhart

      Hi Lorenzo,

      I haven’t run into this issue, but it seems you and plenty others have. In an attempt to fix this, I’ve updated the gist to now only execute on the frontend. I can’t really think of a reason why it should need to happen on the backend Dashboard, so hopefully this is a good fix.

      If you get a chance to re-copy the gist and test it out, please let me know if it fixes the problem.

      Thanks!

Leave a Reply

Your email address will not be published. Required fields are marked *