Skip to content

Instantly share code, notes, and snippets.

@daggerhart
Last active September 22, 2021 10:30
Show Gist options
  • Save daggerhart/c17bdc51662be5a588c9 to your computer and use it in GitHub Desktop.
Save daggerhart/c17bdc51662be5a588c9 to your computer and use it in GitHub Desktop.
<?php
/*
* Note! This is no longer updated here in the Gist. It has been moved to a repo.
*
* @link https://github.com/daggerhart/wp-custom-menu-items
*/
/**
* Class custom_menu_items
*
* This class is for creating and managing dynamic menu items in a WordPress
* menu.
*/
class custom_menu_items {
/**
* Track that hooks have been registered w/ WP
* @var bool
*/
protected $has_registered = false;
/**
* Internal list of menus affected
* @var array
*/
public $menus = array();
/**
* Internal list of new menu items
* @var array
*/
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, $classes = array() ){
$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,
'classes' => $classes,
);
}
/**
* 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, $classes = array() ) {
$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,
'post_parent' => $object->post_parent,
'title' => get_the_title($object),
'url' => get_permalink($object),
'ID' => $ID,
'type' => 'post_type',
'object' => get_post_type($object),
'object_id' => $object_ID,
'classes' => $classes,
);
}
else if ($object_type == 'term') {
global $wpdb;
$sql = "SELECT t.*, tt.taxonomy, tt.parent 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,
'post_parent' => $object->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,
'classes' => $classes,
);
}
}
}
/**
* 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'];
$item_obj->post_parent = !empty( $item['post_parent'] ) ? $item['post_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 = $item['classes'];
$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;
}
}
@daggerhart
Copy link
Author

daggerhart commented Dec 30, 2015

Moved to a repo: wp-custom-menu-items

Related blog post: Dynamically add items to WordPress menus

Example usage:

add_action( 'wp', function(){
    // Single item
    /**
     * @param $menu_slug
     * @param $title
     * @param $url
     * @param $order
     * @param $parent
     * @param null $ID
     */
    custom_menu_items::add_item('menu-1', 'My Profile', get_author_posts_url( get_current_user_id() ), 3  );

    // Item with children
    // note: the ID is manually set for the top level item
    custom_menu_items::add_item('menu-1', 'Top Level', '/some-url', 0, 0, 9876 ); 
    // note: this and other children know the parent ID
    custom_menu_items::add_item('menu-1', 'Child 1', '/some-url/child-1', 0, 9876 ); 
    custom_menu_items::add_item('menu-1', 'Child 2', '/some-url/child-2', 0, 9876 );
    custom_menu_items::add_item('menu-1', 'Child 3', '/some-url/child-3', 0, 9876 );

    /**
     * NEW: Add object by ID
     *
     * @param $menu_slug
     * @param $object_ID
     * @param string $object_type
     * @param $order
     * @param $parent
     * @param null $ID
     */
    // Add the post w/ ID 1 to the menu
    custom_menu_items::add_object('menu-1', 1, 'post');
    // Add the taxonomy term with ID "3" to the menu as a top-level item with the ID of 9876
    custom_menu_items::add_object('menu-1', 3, 'term', 0, 0, 9876);
    // Add the taxonomy term with ID "4" to the menu as a child of item 9876 
    custom_menu_items::add_object('menu-1', 4, 'term', 0, 9876);
} );

@vladcosorg
Copy link

fix_menu_orders is causing ordering issues in WordPress 4.7

@codepuncher
Copy link

@chetzof what issues exactly are they?

Copy link

ghost commented Feb 27, 2017

The issue is that '_sort_nav_menu_items' is depreciated by Wordpress since 4.7.

For solving this issue, update the function fix_menu_orders.

 /**
 * 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.
 *
 * @param $items
 *
 * @return mixed
 */ 
private function fix_menu_orders( $items ) {

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

    wp_list_sort($items, $items[ $i ]->menu_order, 'ASC');

    return $items;
}

@codepuncher
Copy link

@damienbuchs that didn't work for me; throws a lot of undefined offsets and non-object errors.
This worked for me:

/**
 * 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.
 *
 * @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;
}

@vandelio
Copy link

Very awesome class you build here.
I had a need to programmatically add the newly created custom post type items, to the default nav menu.
So that i have a parent child relation, foreach of my cpt's, withone having the user manually adding to the menu.

I only problem i face now, is the class which is added to the new sub-menu points.
Normally when a posttype item is added manually to the wp_nav menu, the class added will match the posttype its in.
like so : menu-item-type-projects menu-item-object-projects

But when i use this awesome class, the classes is added to the li without the posttype in the end,
like so : menu-item-type- menu-item-object-

Is there a way to hook into the function which adds the classes, in order to add the correct posttype extension to the menu item.

@daggerhart
Copy link
Author

@codepuncher - thanks for the updated method, I've added it to the gist.

@vandelio - I've added a new entry point for adding Posts and Taxonomy terms to the gist, and updated the example with its usage. See custom_menu_items::add_object()

@webdados
Copy link

webdados commented Feb 6, 2019

When adding posts or taxonomy terms with add_object, this notice will happen for the currently active menu item:
Notice: Undefined property: stdClass::$post_parent in .../wp-includes/nav-menu-template.php on line 415

To avoid this, we must add a post_parent property to the $item_obj object on the make_item_obj method.

From the tests I've done adding both posts and taxonomy terms via the regular Appearance > Menus tool at wp-admin, I've come to the conclusion that post_parent is the actual post_parent for posts and the term parent for taxonomy terms.

So this is what needs to be done:

  1. At add_object: Add post_parent as $object->post_parent to the menu_item when $object_type == 'post'

  2. At add_object: Add tt.parent to the SQL query at add_object when $object_type == 'term'

  3. At add_object: Add post_parent as $object->parent to the menu_item when $object_type == 'term'

  4. At make_item_obj: Add $item_obj->post_parent as !empty( $item['post_parent'] ) ? $item['post_parent'] : ''

You can find the full class with the changes at https://gist.github.com/webdados/e069ce33947a0e981aede33c79a4d837

@daggerhart
Copy link
Author

Thanks for digging into that @webdados!

Those changes make great sense. I've updated this gist to reflect the changes. Very much appreciated!

@webdados
Copy link

webdados commented Feb 6, 2019

You're welcome @daggerhart

@webdados
Copy link

@daggerhart
Copy link
Author

@webdados
Copy link

Nice!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment