Make menu structure match page heirarchy on page parent change

I have approached this problem and think I have the solution. Please feel free to comment making amendments or improvements to the below!

This functionality can be looked at as a series of 5 steps:

  1. Gather info about the move (the child, its old parent and its new parent)
  2. Find the appropriate menu items (the child, its old parent and its new parent)
  3. Move the child to the appropriate new parent

 

1. Gather info about the move

First, we need to get a copy of the page before and after its move. I’ve done this with two hooks: pre_post_update and save_post_page. In the event that the parent has changed (since these hooks would run on every page update), it calls a custom function called menu_match which I will document in subsequent steps.

// Re-initialise post vars to prevent accidental moves
$old_post = NULL;
$new_post = NULL;

// Capture the post details before they change
add_action('pre_post_update', 'get_old_post_version');
function get_old_post_version($postID){
    global $old_post;
    $old_post = get_post($postID);
}

// Get the updated post details
add_action('save_post_page', 'get_new_post_version');
function get_new_post_version($postID){
    global $old_post, $new_post;
    $new_post = get_post($postID);

    // If the parent has changed, make the menu match the new heirarchy
    if($old_post->post_parent != $new_post->post_parent){
        menu_match($old_post, $new_post);
    }
}

 

2. Find the appropriate menu items

The code for this step and step 3 will be all below step 3, as they are part of the same function and I think it will read better to keep them together.

This step consists of getting the items for the given menu (the ID of which would be specified for the menu you wish to attach to the page heirarchy) and then pushing them to the one array (for the child) if their object_id (the ID of the item they represent) matches the old page’s ID, or to another array (for the parent) if their object_id matches the old page’s post_parent (the ID of the page which is the parent of the old page).

Note: I say “old page”, meaning the old version of the page. There is no “delete and recreate” here.

In case more than one child and/or parent item was found, we match up the correct pairing by checking if the child item’s menu_item_parent (the ID of its parent menu item) matches the parent’s ID… simple, right?

Finally, we find the new parent (the item whose object_id matches the new page’s post_parent).

 

3. Move the child to the appropriate new parent

Simply enough, this step just involves calling wp_update_nav_menu_item to update the child item with the new parent ID. I also reinstated some other properties for sanity.

 

The menu_match function is below – this is my first “answer your own question” submission, so please let me know if I’ve approached this in completely the wrong way! There’s also a couple of “TODO” comments which suggest improvements which could be made on the function… I’m sure there will be a cleaner way to do some of this, but this is the way I’ve used!

function menu_match(&$old_post, &$new_post){
    // Set menu which we're manipulating...
    $menuID = 2;
    // ... and get its items
    $menu_items = wp_get_nav_menu_items($menuID);

    // Find all items which link to the page in question
    $old_child_items = array();
    foreach($menu_items as $m_item){
        if($m_item->object_id == $old_post->ID){
            array_push($old_child_items, $m_item);  
        }
    }

    // If the post isn't top level, find all items which link to the parent in question
    $old_parent_items = array();
    if($old_post->post_parent != 0){
        foreach($menu_items as $m_item){
            if($m_item->object_id == $old_post->post_parent){
                array_push($old_parent_items, $m_item);
            }
        }
    }

    // Pick the correct child (and parent if necessary)
    $child = NULL;
    $old_parent = NULL;
    foreach($old_child_items as $child_candidate){
        if(count($old_parent_items) > 0){
            // If there are parent items to look through, find the one which has the right ID
            foreach($old_parent_items as $parent_candidate){
                if($child_candidate->menu_item_parent == $parent_candidate->ID){
                    $child = $child_candidate;
                    $old_parent = $parent_candidate;
                }
            }
        }else{
            // If there are no parent items, pick the child item which has no parent
            if($child_candidate->menu_item_parent == 0){
                $child = $child_candidate;
            }
        }
    }

    if($child != NULL){
        // If the post isn't moving to top level, find all items which link to the new parent
        if($new_post->post_parent != 0){
            $new_parent_items = array();
            foreach($menu_items as $m_item){
                if($m_item->object_id == $new_post->post_parent){
                    array_push($new_parent_items, $m_item);
                }
            }

            // TODO:    Add recursive upwards search in the event that there are two menu items
            //          for the desired parent.
            //
            // For now, assume the first match is correct, as there shouldn't be more than one
            // item pointing to the same parent.
            if(count($new_parent_items) > 0){
                $new_parentID = $new_parent_items[0]->ID;
            }else{
                // If no matches are found, put the item on the top level
                $new_parentID = 0;  
            }
        }else{
            // Post is moving to top level
            $new_parent_items = NULL;
            $new_parentID = 0;  
        }

        // Update the menu item with the new parent (and reinstate existing properties for sanity)
        $menu_update = wp_update_nav_menu_item($menuID, $child->db_id, array(
            'menu-item-status' => $child->post_status,
            'menu-item-post-name' => $child->post_name,
            'menu-item-type' => $child->post_type,
            'menu-item-parent-id' => $new_parentID,
            'menu-item-object-id' => $child->object_id,
            'menu-item-object' => $child->object,
            'menu-item-type' => $child->type,
            'menu-item-type-label' => $child->type_label,
            'menu-item-url' => $child->url
        ));
    }

    // TODO:    Handle overrides (in case a page doesn't want putting in the menu or
    //          moving from its current location)

    return;
}