Alex Standiford

Create Your Own WordPress Menu API

☕️ 7 min read

What We’ll Be Doing

We’re going to create 3 REST API endpoints, designed to allow us to get our menus in a couple of different ways.

  1. Get a single menu by its ID
  2. Get a single menu by its name (slug)
  3. Get a single menu by its theme location

We’re going to create a single class, NavMenu, to handle these functions.

I should mention that one limitation this build has is that it doesn’t provide a way to filter results by the parent. I didn’t use submenus in my personal build of my site, so I didn’t bother adding this functionality. Children menu items will show up with this build, but you will have to filter out the results with Javascript’s array.filter() method, or just build that directly into the endpoint via an extra argument, or something like that. I’m sure there’s a better database query that can be run to only get the results where the parent is X, but that’s outside of the scope of this lesson.

Build Your NavMenu Class

In my experience, it’s generally easier to create the class that will handle the functionality first, and then build the static methods that will be used as the api callbacks later. Create a new file called NavMenu.php and drop it in your plugin. Be sure to include it in your core plugin file!

<?php
/**
 * Gets a nav menu
 * @author: Alex Standiford
 * @date : 1/13/18
 */

namespace asmenus\lib\app;

class NavMenu{
 
 public function __construct($menu_name_or_id{
    $this->menus = wp_get_nav_menu_items($menu_name_or_id);
 }
 
}
?>

We’ll start simple, and grab the list of menu items using wp_get_nav_menu_items() Luckily, this function will work with either a menu name, or a menu ID, which makes our constructor pretty simple for now.

Also note that we are using a namespace called asmenus\lib\app. This helps to reduce the probability of name collisions with our code and other plugins, and is why I didn’t name the class something like ASNavMenu.

Right now, if we run var_dump(new asmenus\lib\app\NavMenu($menu_name_or_id)); where you replace $menu_name_or_id with a valid menu name or ID, it will dump out all of the relevant information related to the items in that menu.

Create a Way To Get a Menu By its Location

Now that we have a way to get the menu by the menu ID and the menu slug, we need to create a way to get a menu by its location. With a little digging, I discovered that the menu locations and their correlated menu IDs are stored in the database as a theme mod. To access that, we just need to add a method to our class.

<?php
/**
 * Gets a nav menu
 * @author: Alex Standiford
 * @date : 1/13/18
 */

namespace asmenus\lib\app;

class NavMenu{

 public function __construct($menu_name_or_id){
    $this->menus = wp_get_nav_menu_items($menu_name_or_id);
 }

 /**
 * Gets the menu ID from the specified location
 *
 * @param $location
 *
 * @return mixed
 */
 public function getMenuIdFromLocation($location){
    $locations = get_theme_mod('nav_menu_locations');
    
    return $locations[$location];
 }

}
?>

Here, we added a new method, getMenuIdFromLocation. This method uses get_theme_mod returns an associative array, with the menu IDs keyed by the menu location. So, by passing a $location parameter, we can get the desired menu ID.

Now, we just need to use this function in our constructor.

<?php
/**
 * Gets a nav menu
 * @author: Alex Standiford
 * @date : 1/13/18
 */

namespace asmenus\lib\app;

class NavMenu{

 public function __construct($menu_name_or_id, $type = 'menu'){
    if($type == 'location') $menu_name_or_id = $this->getMenuIdFromLocation($menu_name_or_id);
    
    $this->menus = wp_get_nav_menu_items($menu_name_or_id);
 }

 /**
 * Gets the menu ID from the specified location
 *
 * @param $location
 *
 * @return mixed
 */
 public function getMenuIdFromLocation($location){
    $locations = get_theme_mod('nav_menu_locations');
    
    return $locations[$location];
 }

}
?>

On our constructor, we add an optional second argument, $type, which if the type is set to 'location', our constructor will use the getMenuIdFromLocation to get the correct menu ID based on the provided location in our first argument, $menu_name_or_id.

Right now, if we run var_dump(new asmenus\lib\app\NavMenu($menu_location,'location')); where you replace $menu_location with a valid menu location, it will dump out all of the relevant information related to the items in that menu.

Create the REST API Callback Functions

Now that our class is capable of getting our menu details, let’s create our functions that will be used by our rest endpoints when our endpoint is visited.

<?php
/**
 * Gets a nav menu
 * @author: Alex Standiford
 * @date : 1/13/18
 */

namespace asmenus\lib\app;

class NavMenu{

 public function __construct($menu_name_or_id, $type = 'menu'){
    if($type == 'location') $menu_name_or_id = $this->getMenuIdFromLocation($menu_name_or_id);
    
    $this->menus = wp_get_nav_menu_items($menu_name_or_id);
 }

 /**
 * Gets the menu ID from the specified location
 *
 * @param $location
 *
 * @return mixed
 */
 public function getMenuIdFromLocation($location){
    $locations = get_theme_mod('nav_menu_locations');
    
    return $locations[$location];
 }


 /**
 * Callback function to get the menu by name/id via the REST API
 *
 * @param \WP_REST_Request $request
 *
 * @return array|\WP_Error
 */
 public static function getMenuFromApi(\WP_REST_Request $request){
    $name_or_id = $request->get_param('name') ? $request->get_param('name') : (int) $request->get_param('id');
    $self = new self($name_or_id);
    
    return $self->menus;
 }

}
?>

We’ll start with the first method, which will handle getting the menu regardless of if it is a name or an id. As you can see, this is a static method (in other words, we can access it without instantiating our class first), and the first parameter is type-hinted with \WP_REST_Request. If you’re using a namespace, be sure to remember to add the \ beforehand, otherwise it won’t work.

$name_or_id is set with a ternary operator – which allows us to get the argument regardless of if it is a name or an id. You could have built this out as two separate methods, but I felt this was cleaner.

Our Completed Class

Now, we just need to repeat this process with a second static method that will get the menu by location.

<?php
/**
 * Gets a nav menu
 * @author: Alex Standiford
 * @date : 1/13/18
 */

namespace asmenus\lib\app;

class NavMenu{

 public function __construct($menu_name_or_id, $type = 'menu'){
    if($type == 'location') $menu_name_or_id = $this->getMenuIdFromLocation($menu_name_or_id);
    
    $this->menus = wp_get_nav_menu_items($menu_name_or_id);
 }

 /**
 * Gets the menu ID from the specified location
 *
 * @param $location
 *
 * @return mixed
 */
 public function getMenuIdFromLocation($location){
    $locations = get_theme_mod('nav_menu_locations');
    
    return $locations[$location];
 }


 /**
 * Callback function to get the menu by slug/id via the REST API
 *
 * @param \WP_REST_Request $request
 *
 * @return array|\WP_Error
 */
 public static function getMenuFromApi(\WP_REST_Request $request){
    $name_or_id = $request->get_param('name') ? $request->get_param('name') : (int) $request->get_param('id');
    $self = new self($name_or_id);
    
    return $self->menus;
 }


 /**
 * Callback function to get the menu by location via the REST API
 *
 * @param \WP_REST_Request $request
 *
 * @return array|\WP_Error
 */
 public static function getMenuByLocationFromApi(\WP_REST_Request $request){
    $location = $request->get_param('location');
    $self = new self($location, "location");
    
    return $self->menus;
 }

}
?>

The only difference here is that when we instantiated the class, we set the second parameter to 'location'. This tells our constructor to look up the ID based on the location provided

Register Our Endpoints

Now that our class is built out, we just need to register our endpoints. This should not be added to NavMenu.php, instead this should be either placed in the core plugin file, or in a separate file that is included afterward. In the example below, I put them in the core plugin file.

<?php
/**
Plugin Name: WordPress Menu REST API Endpoints
Description: Creates RESTful endpoints for WordPress menus
Version: 1.0
Author: Alex Standiford
Author URI: http://www.alexstandiford.com
**/

if(!defined('ABSPATH')) exit;

require_once(plugin_dir_path(__FILE__).'NavMenu.php');


/**
 * Registers our api endpoints
 */
add_action('rest_api_init', function(){
 /**
 * Gets a single menu by ID
 */
 register_rest_route('asmenus/v1', '/menu/(?P<id>[\d]+)', [
    'methods' => ['GET'],
    'callback' => 'asmenus\lib\app\NavMenu::getMenuFromApi',
 ]);

 /**
 * Gets a single menu by slug
 */
 register_rest_route('asmenus/v1', '/menu/(?P<name>[\w-_]+)', [
    'methods' => ['GET'],
    'callback' => 'asmenus\lib\app\NavMenu::getMenuFromApi',
 ]);

 /**
 * Gets a single menu by its theme location
 */
 register_rest_route('asmenus/v1', '/menu/location/(?P<location>[\w-_]+)', [
    'methods' => ['GET'],
    'callback' => 'asmenus\lib\app\NavMenu::getMenuByLocationFromApi',
 ]);
});

?>

To register our endpoints, we hook into 'rest_api_init', and register our routes in the callback, using register_rest_route. Notice that our callbacks have the namespace prepended to them, and that we are setting the method for these endpoints to GET.

Voila! When you visit any of the endpoints, you should now get an array of objects containing all of the information about each menu item. From here, you could build out some React/Angular components that loop through and build your menu.