WordPress Menus – filtering pages

In this tutorial I will show how to use Walker classes to filter which pages to display in a menu.

The problem first occurred because I wanted to use the Member Access plugin to mark some pages as “Members Only”, so therefore only accessible to users that are logged in. The plugin redirects users to a login page if they try to access a members-only page.

However, the members-only pages still appear on the menu.

I could have had two separate Nav Menus, one for logged in users and one for logged out users, but this is clumsy and requires two menus to be kept up to date. I also wanted to be able to fall back on using Page Menus (see previous article).

So, I decided instead to write some filters which would filter out members-only pages for logged out users. I wanted to do this for Nav Menus – so there only needs to be one Nav Menu which includes the members-only links knowing they will only be shown to members. I also wanted to do it for Page Menus.

Filter Function

The first stage is to implement a filter function that, given a post or page ID, returns a boolean indicating whether to keep it hidden or not. The filter logic is:

function ajr_hide_id($id)
{
  // if user is logged in they can see everything
  if (is_user_logged_in()) return false;
  // if the Members Access plugin is not installed, then no filtering
  if (!function_exists('member_access_is_private')) return false;
  // else filter using the exported function
  return member_access_is_private($id);
}

So, a logged-in user can see everything. If the Member Access plugin is not installed, then the user can see everything. Otherwise the plugin’s member_access_is_private function is used to decide whether to hide the page or post.

The next stage is to use this function to filter Menus.

Page Menus

I’ll start with the Page Menu filter since that is the simpler one.

The wp_page_menu function takes as its ‘walker’ parameter, an object of the Walker class. The Walker class cannot be used itself, it is an abstract class, but you use a class inherited from it that is suited to the type of menu. The walker for wp_page_menu should be a Walker_Page object.

To customise the walker for Page Menus, create a new class which is inherited from Walker_Page and change its behaviour by overloading its virtual methods. The outline class is:

class ajr_members_page_walker extends Walker_Page
{
  function display_element($element, &$children_elements, $max_depth, $depth, $args, &$output)
  {
  }
}

In this case I am overloading the display_element method. This method displays a page in the menu by generating a LI element. Normally, the new element is appended as a string to the $output parameter. To skip an element, simply don’t append anything.

The code is:

  function display_element($element, &$children_elements, $max_depth, $depth, $args, &$output)
  {
    if (ajr_hide_id($element->ID)) return;
    parent::display_element($element, $children_elements, $max_depth, $depth, $args, $output);
  }

So, applying the filter is simply a case of returning immediately if the filter function indicates we should hide that element. Otherwise, call the Walker_Page::display_element to do the actual display.

This is now a complete members-only filtering walker and can be applied to the wp_page_menu function:

wp_page_menu(array('walker' => new ajr_members_page_walker...));

Nav Menus

It would seem that filtering on a Nav Menu would be much the same as on a Page Menu. But this is not so, since a Nav Menu can contain elements which are not pages. So the Nav Menu contains a special type of post of type nav_menu_item which then contains a reference to the linked object.

This extra level of indirection means that the filter function as it stands will not work on the Nav Menu items themselves – the Member Access plugin puts the public/private flag on the page itself, not any Nav Menu items that refer to it.

This extra complexity is also reflected in the walker object used on Nav Menus. This must be of class Walker_Nav_Menu, not Walker_Page.

So, a new class must be defined for filtering Nav Menus, which inherits from Walker_Nav_Menu:

class ajr_members_nav_walker extends Walker_Nav_Menu
{
  function display_element($element, &$children_elements, $max_depth, $depth, $args, &$output)
  {
  }
}

The implementation of the display_element in this case is slightly different from the Page Menu version. We must check that the menu item is referring to a post, then test whether that post should be hidden:

  function display_element($element, &$children_elements, $max_depth, $depth, $args, &$output)
  {
    if (($element->type == 'post_type') && ajr_hide_id($element->object_id)) return;
    parent::display_element($element, $children_elements, $max_depth, $depth, $args, $output);
  }

Note that we don’t use the element’s ‘ID’ field – that is the ID of the nav_menu_item. Instead we use the ‘element_id’ field, which contains the referred-to post ID, but only if the item type is a post (it refers to a category ID if the type is category etc.).

This can now be used to filter a Nav Menu:

wp_nav_menu(array('walker' => new ajr_members_nav_walker...));

The Fallback Gotcha

When using wp_nav_menu, bear in mind that, if a Nav Menu hasn’t been defined, then the function falls back on the function specified by its ‘fallback_cb’ argument, which defaults to the wp_page_menu function. When this fallback occurs, all the arguments of wp_nav_menu are passed on to the wp_page_menu function. Note that both functions can take a ‘walker’ argument. However, these are fundamentally different and incompatible. For a Nav Menu, you need a Walker_Nav_Menu whereas for a Page Menu, you need a Walker_Page and these are not compatible – a Walker_Nav_Menu is not inherited from Walker_Page.

So, if you call wp_nav_menu with a walker which is a Walker_Nav_Menu, and the function calls the fallback, then wp_page_menu will be called with a Walker_Nav_Menu walker. Which will fail.

So, you need to separate out the Nav Menu and Page Menu calls, disabling the fallback behaviour:

  if (has_nav_menu('primary'))
  {
    // Use a Navigation menu
    wp_nav_menu(array('theme_location' => 'primary', 'fallback_cb' => false, 'walker' => new ajr_members_nav_walker));
  }
  else
  {
    // Use a Page menu
    wp_page_menu(array('walker' => new ajr_members_page_walker));
  }

This will now work correctly whether or not the theme user has defined a Nav Menu.

Leave a Reply