From 3415575d58d325a2b57063f7e339127de03af1c2 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 18 Mar 2026 08:45:51 +0000 Subject: [PATCH 01/11] Initial plan From e175a73e91cb4180e9d886b1dfe164edb2e3fe2e Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 18 Mar 2026 08:52:48 +0000 Subject: [PATCH 02/11] Fix position not updated in wp menu item update command Co-authored-by: swissspidy <841956+swissspidy@users.noreply.github.com> --- features/menu-item.feature | 46 ++++++++++++++++++++++++++++++++++++++ src/Menu_Item_Command.php | 30 ++++++++++++++++++++++++- 2 files changed, 75 insertions(+), 1 deletion(-) diff --git a/features/menu-item.feature b/features/menu-item.feature index c5a040568..6d4cc407d 100644 --- a/features/menu-item.feature +++ b/features/menu-item.feature @@ -195,6 +195,52 @@ Feature: Manage WordPress menu items | custom | First | 1 | https://first.com | | custom | Third | 2 | https://third.com | + Scenario: Menu order is recalculated on update + When I run `wp menu create "Sidebar Menu"` + Then STDOUT should not be empty + + When I run `wp menu item add-custom sidebar-menu Alpha https://alpha.com --porcelain` + Then save STDOUT as {ITEM_ID_1} + + When I run `wp menu item add-custom sidebar-menu Beta https://beta.com --porcelain` + Then save STDOUT as {ITEM_ID_2} + + When I run `wp menu item add-custom sidebar-menu Gamma https://gamma.com --porcelain` + Then save STDOUT as {ITEM_ID_3} + + When I run `wp menu item list sidebar-menu --fields=type,title,position,link` + Then STDOUT should be a table containing rows: + | type | title | position | link | + | custom | Alpha | 1 | https://alpha.com | + | custom | Beta | 2 | https://beta.com | + | custom | Gamma | 3 | https://gamma.com | + + When I run `wp menu item update {ITEM_ID_3} --position=1` + Then STDOUT should be: + """ + Success: Menu item updated. + """ + + When I run `wp menu item list sidebar-menu --fields=type,title,position,link` + Then STDOUT should be a table containing rows: + | type | title | position | link | + | custom | Gamma | 1 | https://gamma.com | + | custom | Alpha | 2 | https://alpha.com | + | custom | Beta | 3 | https://beta.com | + + When I run `wp menu item update {ITEM_ID_1} --position=3` + Then STDOUT should be: + """ + Success: Menu item updated. + """ + + When I run `wp menu item list sidebar-menu --fields=type,title,position,link` + Then STDOUT should be a table containing rows: + | type | title | position | link | + | custom | Gamma | 1 | https://gamma.com | + | custom | Beta | 2 | https://beta.com | + | custom | Alpha | 3 | https://alpha.com | + Scenario: Get menu item details When I run `wp menu create "Sidebar Menu"` Then STDOUT should not be empty diff --git a/src/Menu_Item_Command.php b/src/Menu_Item_Command.php index 94a1ea3f3..cf2c13cd3 100644 --- a/src/Menu_Item_Command.php +++ b/src/Menu_Item_Command.php @@ -572,6 +572,8 @@ private function add_or_update_item( $method, $type, $args, $assoc_args ) { // `url` is protected in WP-CLI, so we use `link` instead $assoc_args['url'] = Utils\get_flag_value( $assoc_args, 'link' ); + $old_position = 0; + // Need to persist the menu item data. See https://core.trac.wordpress.org/ticket/28138 if ( 'update' === $method ) { @@ -587,7 +589,8 @@ private function add_or_update_item( $method, $type, $args, $assoc_args ) { $menu_item_obj = wp_setup_nav_menu_item( $menu_item_obj ); // Correct the menu position if this was the first item. See https://core.trac.wordpress.org/ticket/28140 - $position = ( 0 === $menu_item_obj->menu_order ) ? 1 : $menu_item_obj->menu_order; + $position = ( 0 === $menu_item_obj->menu_order ) ? 1 : $menu_item_obj->menu_order; + $old_position = $position; $default_args = [ 'position' => $position, @@ -648,6 +651,15 @@ private function add_or_update_item( $method, $type, $args, $assoc_args ) { if ( ( 'add' === $method ) && $menu_item_args['menu-item-position'] ) { $this->reorder_menu_items( $menu->term_id, $menu_item_args['menu-item-position'], +1, $result ); + } elseif ( 'update' === $method ) { + $new_position = (int) $menu_item_args['menu-item-position']; + if ( $new_position > 0 && $new_position !== $old_position ) { + if ( $new_position < $old_position ) { + $this->reorder_menu_items_in_range( $menu->term_id, $new_position, $old_position - 1, +1, $result ); + } else { + $this->reorder_menu_items_in_range( $menu->term_id, $old_position + 1, $new_position, -1, $result ); + } + } } /** @@ -688,6 +700,22 @@ private function reorder_menu_items( $menu_id, $min_position, $increment, $ignor return $wpdb->query( $wpdb->prepare( "UPDATE $wpdb->posts SET `menu_order`=`menu_order`+(%d) WHERE `menu_order`>=%d AND ID IN (SELECT object_id FROM $wpdb->term_relationships WHERE term_taxonomy_id=%d) AND ID<>%d", (int) $increment, (int) $min_position, (int) $menu_id, (int) $ignore_item_id ) ); } + /** + * Move a range of items in one nav_menu up or down by incrementing/decrementing their menu_order field. + * Only affects items with menu_order between min_position and max_position (inclusive). + * + * @param int $menu_id ID of the nav_menu + * @param int $min_position minimal menu_order to touch (inclusive) + * @param int $max_position maximal menu_order to touch (inclusive) + * @param int $increment how much to change menu_order: +1 to move down, -1 to move up + * @param int $ignore_item_id menu item that should be ignored by the change (e.g. the updated menu item) + * @return int|false number of rows affected, or false on failure + */ + private function reorder_menu_items_in_range( $menu_id, $min_position, $max_position, $increment, $ignore_item_id = 0 ) { + global $wpdb; + return $wpdb->query( $wpdb->prepare( "UPDATE $wpdb->posts SET `menu_order`=`menu_order`+(%d) WHERE `menu_order` BETWEEN %d AND %d AND ID IN (SELECT object_id FROM $wpdb->term_relationships WHERE term_taxonomy_id=%d) AND ID<>%d", (int) $increment, (int) $min_position, (int) $max_position, (int) $menu_id, (int) $ignore_item_id ) ); + } + protected function get_formatter( &$assoc_args ) { return new Formatter( $assoc_args, $this->obj_fields ); } From d1110f17ed3de2fc235e85f70291173ca1b93ca3 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 18 Mar 2026 09:49:36 +0000 Subject: [PATCH 03/11] Fix position update: reorder items before wp_update_nav_menu_item call Co-authored-by: swissspidy <841956+swissspidy@users.noreply.github.com> --- src/Menu_Item_Command.php | 34 +++++++++++++++++++--------------- 1 file changed, 19 insertions(+), 15 deletions(-) diff --git a/src/Menu_Item_Command.php b/src/Menu_Item_Command.php index cf2c13cd3..55a34a000 100644 --- a/src/Menu_Item_Command.php +++ b/src/Menu_Item_Command.php @@ -572,8 +572,6 @@ private function add_or_update_item( $method, $type, $args, $assoc_args ) { // `url` is protected in WP-CLI, so we use `link` instead $assoc_args['url'] = Utils\get_flag_value( $assoc_args, 'link' ); - $old_position = 0; - // Need to persist the menu item data. See https://core.trac.wordpress.org/ticket/28138 if ( 'update' === $method ) { @@ -637,7 +635,23 @@ private function add_or_update_item( $method, $type, $args, $assoc_args ) { } $menu_item_args['menu-item-type'] = $type; - $result = wp_update_nav_menu_item( $menu->term_id, $menu_item_db_id, $menu_item_args ); + + // Reorder other menu items before updating the current one. + // The item being moved is at $old_position, which is never inside the affected + // range ([new, old-1] for moving up or [old+1, new] for moving down), so no + // exclusion of the item itself is necessary. + if ( 'update' === $method ) { + $new_position = (int) $menu_item_args['menu-item-position']; + if ( $new_position > 0 && $new_position !== $old_position ) { + if ( $new_position < $old_position ) { + $this->reorder_menu_items_in_range( $menu->term_id, $new_position, $old_position - 1, +1 ); + } else { + $this->reorder_menu_items_in_range( $menu->term_id, $old_position + 1, $new_position, -1 ); + } + } + } + + $result = wp_update_nav_menu_item( $menu->term_id, $menu_item_db_id, $menu_item_args ); if ( is_wp_error( $result ) ) { WP_CLI::error( $result->get_error_message() ); @@ -651,15 +665,6 @@ private function add_or_update_item( $method, $type, $args, $assoc_args ) { if ( ( 'add' === $method ) && $menu_item_args['menu-item-position'] ) { $this->reorder_menu_items( $menu->term_id, $menu_item_args['menu-item-position'], +1, $result ); - } elseif ( 'update' === $method ) { - $new_position = (int) $menu_item_args['menu-item-position']; - if ( $new_position > 0 && $new_position !== $old_position ) { - if ( $new_position < $old_position ) { - $this->reorder_menu_items_in_range( $menu->term_id, $new_position, $old_position - 1, +1, $result ); - } else { - $this->reorder_menu_items_in_range( $menu->term_id, $old_position + 1, $new_position, -1, $result ); - } - } } /** @@ -708,12 +713,11 @@ private function reorder_menu_items( $menu_id, $min_position, $increment, $ignor * @param int $min_position minimal menu_order to touch (inclusive) * @param int $max_position maximal menu_order to touch (inclusive) * @param int $increment how much to change menu_order: +1 to move down, -1 to move up - * @param int $ignore_item_id menu item that should be ignored by the change (e.g. the updated menu item) * @return int|false number of rows affected, or false on failure */ - private function reorder_menu_items_in_range( $menu_id, $min_position, $max_position, $increment, $ignore_item_id = 0 ) { + private function reorder_menu_items_in_range( $menu_id, $min_position, $max_position, $increment ) { global $wpdb; - return $wpdb->query( $wpdb->prepare( "UPDATE $wpdb->posts SET `menu_order`=`menu_order`+(%d) WHERE `menu_order` BETWEEN %d AND %d AND ID IN (SELECT object_id FROM $wpdb->term_relationships WHERE term_taxonomy_id=%d) AND ID<>%d", (int) $increment, (int) $min_position, (int) $max_position, (int) $menu_id, (int) $ignore_item_id ) ); + return $wpdb->query( $wpdb->prepare( "UPDATE $wpdb->posts SET `menu_order`=`menu_order`+(%d) WHERE `menu_order` BETWEEN %d AND %d AND ID IN (SELECT object_id FROM $wpdb->term_relationships WHERE term_taxonomy_id=%d)", (int) $increment, (int) $min_position, (int) $max_position, (int) $menu_id ) ); } protected function get_formatter( &$assoc_args ) { From 3245ddcc12f9d81a613623744973b96bfe3f3abc Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 18 Mar 2026 13:47:24 +0000 Subject: [PATCH 04/11] Fix: reorder menu items using normalized rank-based wp_update_post instead of range SQL Co-authored-by: swissspidy <841956+swissspidy@users.noreply.github.com> --- src/Menu_Item_Command.php | 62 +++++++++++++++++++++++++++++++++------ 1 file changed, 53 insertions(+), 9 deletions(-) diff --git a/src/Menu_Item_Command.php b/src/Menu_Item_Command.php index 55a34a000..a2918fc86 100644 --- a/src/Menu_Item_Command.php +++ b/src/Menu_Item_Command.php @@ -636,17 +636,61 @@ private function add_or_update_item( $method, $type, $args, $assoc_args ) { $menu_item_args['menu-item-type'] = $type; - // Reorder other menu items before updating the current one. - // The item being moved is at $old_position, which is never inside the affected - // range ([new, old-1] for moving up or [old+1, new] for moving down), so no - // exclusion of the item itself is necessary. + // Reorder other menu items when the position changes on update. if ( 'update' === $method ) { $new_position = (int) $menu_item_args['menu-item-position']; - if ( $new_position > 0 && $new_position !== $old_position ) { - if ( $new_position < $old_position ) { - $this->reorder_menu_items_in_range( $menu->term_id, $new_position, $old_position - 1, +1 ); - } else { - $this->reorder_menu_items_in_range( $menu->term_id, $old_position + 1, $new_position, -1 ); + if ( $new_position > 0 ) { + // Fetch all menu items sorted by their raw menu_order to determine + // normalized (1-indexed) ranks, since wp_get_nav_menu_items(ARRAY_A) + // normalises menu_order to 1,2,3… which may differ from the raw DB values. + $sorted_items = get_posts( + [ + 'post_type' => 'nav_menu_item', + 'numberposts' => -1, + 'orderby' => 'menu_order', + 'order' => 'ASC', + 'post_status' => 'any', + 'tax_query' => [ + [ + 'taxonomy' => 'nav_menu', + 'field' => 'term_taxonomy_id', + 'terms' => $menu->term_taxonomy_id, + ], + ], + ] + ); + + // Find the 1-indexed normalized rank of the item being moved. + $old_position_normalized = 0; + foreach ( $sorted_items as $idx => $sorted_item ) { + if ( (int) $sorted_item->ID === (int) $menu_item_db_id ) { + $old_position_normalized = $idx + 1; + break; + } + } + + if ( $old_position_normalized > 0 && $new_position !== $old_position_normalized ) { + if ( $new_position < $old_position_normalized ) { + // Moving up: items at 0-indexed [new_pos-1, old_pos-2] shift down by +1. + for ( $i = $new_position - 1; $i <= $old_position_normalized - 2; $i++ ) { + wp_update_post( + [ + 'ID' => $sorted_items[ $i ]->ID, + 'menu_order' => $i + 2, + ] + ); + } + } else { + // Moving down: items at 0-indexed [old_pos, new_pos-1] shift up by -1. + for ( $i = $old_position_normalized; $i <= $new_position - 1; $i++ ) { + wp_update_post( + [ + 'ID' => $sorted_items[ $i ]->ID, + 'menu_order' => $i, + ] + ); + } + } } } } From 5c4a0a2e09907283e73f53a21b0d03cacc6aae7d Mon Sep 17 00:00:00 2001 From: Pascal Birchler Date: Wed, 18 Mar 2026 18:30:07 +0100 Subject: [PATCH 05/11] Lint fixes --- src/Menu_Item_Command.php | 27 ++++++--------------------- 1 file changed, 6 insertions(+), 21 deletions(-) diff --git a/src/Menu_Item_Command.php b/src/Menu_Item_Command.php index a2918fc86..0a83fe9be 100644 --- a/src/Menu_Item_Command.php +++ b/src/Menu_Item_Command.php @@ -645,12 +645,12 @@ private function add_or_update_item( $method, $type, $args, $assoc_args ) { // normalises menu_order to 1,2,3… which may differ from the raw DB values. $sorted_items = get_posts( [ - 'post_type' => 'nav_menu_item', - 'numberposts' => -1, - 'orderby' => 'menu_order', - 'order' => 'ASC', - 'post_status' => 'any', - 'tax_query' => [ + 'post_type' => 'nav_menu_item', + 'numberposts' => -1, + 'orderby' => 'menu_order', + 'order' => 'ASC', + 'post_status' => 'any', + 'tax_query' => [ [ 'taxonomy' => 'nav_menu', 'field' => 'term_taxonomy_id', @@ -749,21 +749,6 @@ private function reorder_menu_items( $menu_id, $min_position, $increment, $ignor return $wpdb->query( $wpdb->prepare( "UPDATE $wpdb->posts SET `menu_order`=`menu_order`+(%d) WHERE `menu_order`>=%d AND ID IN (SELECT object_id FROM $wpdb->term_relationships WHERE term_taxonomy_id=%d) AND ID<>%d", (int) $increment, (int) $min_position, (int) $menu_id, (int) $ignore_item_id ) ); } - /** - * Move a range of items in one nav_menu up or down by incrementing/decrementing their menu_order field. - * Only affects items with menu_order between min_position and max_position (inclusive). - * - * @param int $menu_id ID of the nav_menu - * @param int $min_position minimal menu_order to touch (inclusive) - * @param int $max_position maximal menu_order to touch (inclusive) - * @param int $increment how much to change menu_order: +1 to move down, -1 to move up - * @return int|false number of rows affected, or false on failure - */ - private function reorder_menu_items_in_range( $menu_id, $min_position, $max_position, $increment ) { - global $wpdb; - return $wpdb->query( $wpdb->prepare( "UPDATE $wpdb->posts SET `menu_order`=`menu_order`+(%d) WHERE `menu_order` BETWEEN %d AND %d AND ID IN (SELECT object_id FROM $wpdb->term_relationships WHERE term_taxonomy_id=%d)", (int) $increment, (int) $min_position, (int) $max_position, (int) $menu_id ) ); - } - protected function get_formatter( &$assoc_args ) { return new Formatter( $assoc_args, $this->obj_fields ); } From dc9a9030a77ee1153a1ef047ecb228b9766a73a0 Mon Sep 17 00:00:00 2001 From: Pascal Birchler Date: Wed, 18 Mar 2026 19:56:55 +0100 Subject: [PATCH 06/11] Potential fix for pull request finding Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --- src/Menu_Item_Command.php | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/Menu_Item_Command.php b/src/Menu_Item_Command.php index 0a83fe9be..ef5d2d8b6 100644 --- a/src/Menu_Item_Command.php +++ b/src/Menu_Item_Command.php @@ -587,8 +587,7 @@ private function add_or_update_item( $method, $type, $args, $assoc_args ) { $menu_item_obj = wp_setup_nav_menu_item( $menu_item_obj ); // Correct the menu position if this was the first item. See https://core.trac.wordpress.org/ticket/28140 - $position = ( 0 === $menu_item_obj->menu_order ) ? 1 : $menu_item_obj->menu_order; - $old_position = $position; + $position = ( 0 === $menu_item_obj->menu_order ) ? 1 : $menu_item_obj->menu_order; $default_args = [ 'position' => $position, From 67bc91b9d2fa7cc79f4cc84f144e9cc205696578 Mon Sep 17 00:00:00 2001 From: Pascal Birchler Date: Wed, 18 Mar 2026 19:57:17 +0100 Subject: [PATCH 07/11] Potential fix for pull request finding Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --- src/Menu_Item_Command.php | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/src/Menu_Item_Command.php b/src/Menu_Item_Command.php index ef5d2d8b6..675822e59 100644 --- a/src/Menu_Item_Command.php +++ b/src/Menu_Item_Command.php @@ -659,6 +659,13 @@ private function add_or_update_item( $method, $type, $args, $assoc_args ) { ] ); + // Clamp the requested position to the valid range of menu items. + $max_position = count( $sorted_items ); + if ( $max_position > 0 && $new_position > $max_position ) { + // Treat out-of-range positions as "move to end", consistent with core behavior. + $new_position = $max_position; + } + // Find the 1-indexed normalized rank of the item being moved. $old_position_normalized = 0; foreach ( $sorted_items as $idx => $sorted_item ) { From bb9e9b21bedeb619e56fb6c2b7e1b35187aaa4c2 Mon Sep 17 00:00:00 2001 From: Pascal Birchler Date: Wed, 18 Mar 2026 21:09:17 +0100 Subject: [PATCH 08/11] Potential fix for pull request finding Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --- src/Menu_Item_Command.php | 25 +++++++++++++------------ 1 file changed, 13 insertions(+), 12 deletions(-) diff --git a/src/Menu_Item_Command.php b/src/Menu_Item_Command.php index 675822e59..48a31888d 100644 --- a/src/Menu_Item_Command.php +++ b/src/Menu_Item_Command.php @@ -634,6 +634,7 @@ private function add_or_update_item( $method, $type, $args, $assoc_args ) { } $menu_item_args['menu-item-type'] = $type; + $pending_menu_order_updates = []; // Reorder other menu items when the position changes on update. if ( 'update' === $method ) { @@ -679,22 +680,18 @@ private function add_or_update_item( $method, $type, $args, $assoc_args ) { if ( $new_position < $old_position_normalized ) { // Moving up: items at 0-indexed [new_pos-1, old_pos-2] shift down by +1. for ( $i = $new_position - 1; $i <= $old_position_normalized - 2; $i++ ) { - wp_update_post( - [ - 'ID' => $sorted_items[ $i ]->ID, - 'menu_order' => $i + 2, - ] - ); + $pending_menu_order_updates[] = [ + 'ID' => $sorted_items[ $i ]->ID, + 'menu_order' => $i + 2, + ]; } } else { // Moving down: items at 0-indexed [old_pos, new_pos-1] shift up by -1. for ( $i = $old_position_normalized; $i <= $new_position - 1; $i++ ) { - wp_update_post( - [ - 'ID' => $sorted_items[ $i ]->ID, - 'menu_order' => $i, - ] - ); + $pending_menu_order_updates[] = [ + 'ID' => $sorted_items[ $i ]->ID, + 'menu_order' => $i, + ]; } } } @@ -712,6 +709,10 @@ private function add_or_update_item( $method, $type, $args, $assoc_args ) { WP_CLI::error( "Couldn't update menu item." ); } } else { + // Apply deferred reordering of other menu items only after a successful update. + foreach ( $pending_menu_order_updates as $update_args ) { + wp_update_post( $update_args ); + } if ( ( 'add' === $method ) && $menu_item_args['menu-item-position'] ) { $this->reorder_menu_items( $menu->term_id, $menu_item_args['menu-item-position'], +1, $result ); From c7dfdf06a888122eb56dffdfbe74ca70133af50d Mon Sep 17 00:00:00 2001 From: Pascal Birchler Date: Thu, 19 Mar 2026 11:27:23 +0100 Subject: [PATCH 09/11] Apply suggestions from code review Co-authored-by: Pascal Birchler --- src/Menu_Item_Command.php | 122 +++++++++++++++++++++----------------- 1 file changed, 67 insertions(+), 55 deletions(-) diff --git a/src/Menu_Item_Command.php b/src/Menu_Item_Command.php index 48a31888d..a349d545c 100644 --- a/src/Menu_Item_Command.php +++ b/src/Menu_Item_Command.php @@ -643,58 +643,54 @@ private function add_or_update_item( $method, $type, $args, $assoc_args ) { // Fetch all menu items sorted by their raw menu_order to determine // normalized (1-indexed) ranks, since wp_get_nav_menu_items(ARRAY_A) // normalises menu_order to 1,2,3… which may differ from the raw DB values. - $sorted_items = get_posts( - [ - 'post_type' => 'nav_menu_item', - 'numberposts' => -1, - 'orderby' => 'menu_order', - 'order' => 'ASC', - 'post_status' => 'any', - 'tax_query' => [ - [ - 'taxonomy' => 'nav_menu', - 'field' => 'term_taxonomy_id', - 'terms' => $menu->term_taxonomy_id, - ], - ], - ] - ); - - // Clamp the requested position to the valid range of menu items. - $max_position = count( $sorted_items ); - if ( $max_position > 0 && $new_position > $max_position ) { - // Treat out-of-range positions as "move to end", consistent with core behavior. - $new_position = $max_position; - } - - // Find the 1-indexed normalized rank of the item being moved. - $old_position_normalized = 0; - foreach ( $sorted_items as $idx => $sorted_item ) { - if ( (int) $sorted_item->ID === (int) $menu_item_db_id ) { - $old_position_normalized = $idx + 1; - break; - } - } - - if ( $old_position_normalized > 0 && $new_position !== $old_position_normalized ) { - if ( $new_position < $old_position_normalized ) { - // Moving up: items at 0-indexed [new_pos-1, old_pos-2] shift down by +1. - for ( $i = $new_position - 1; $i <= $old_position_normalized - 2; $i++ ) { - $pending_menu_order_updates[] = [ - 'ID' => $sorted_items[ $i ]->ID, - 'menu_order' => $i + 2, - ]; - } - } else { - // Moving down: items at 0-indexed [old_pos, new_pos-1] shift up by -1. - for ( $i = $old_position_normalized; $i <= $new_position - 1; $i++ ) { - $pending_menu_order_updates[] = [ - 'ID' => $sorted_items[ $i ]->ID, - 'menu_order' => $i, - ]; - } - } - } + $sorted_item_ids = get_posts( + [ + 'post_type' => 'nav_menu_item', + 'numberposts' => -1, + 'orderby' => 'menu_order', + 'order' => 'ASC', + 'post_status' => 'any', + 'tax_query' => [ + [ + 'taxonomy' => 'nav_menu', + 'field' => 'term_taxonomy_id', + 'terms' => $menu->term_taxonomy_id, + ], + ], + 'fields' => 'ids', + ] + ); + + // Clamp the requested position to the valid range of menu items. + $max_position = count( $sorted_item_ids ); + if ( $max_position > 0 && $new_position > $max_position ) { + // Treat out-of-range positions as "move to end", consistent with core behavior. + $new_position = $max_position; + } + + // Find the 1-indexed normalized rank of the item being moved. + $item_idx = array_search( (string) $menu_item_db_id, $sorted_item_ids, true ); + $old_position_normalized = ( false !== $item_idx ) ? $item_idx + 1 : 0; + + if ( $old_position_normalized > 0 && $new_position !== $old_position_normalized ) { + if ( $new_position < $old_position_normalized ) { + // Moving up: items at 0-indexed [new_pos-1, old_pos-2] shift down by +1. + for ( $i = $new_position - 1; $i <= $old_position_normalized - 2; $i++ ) { + $pending_menu_order_updates[] = [ + 'ID' => $sorted_item_ids[ $i ], + 'menu_order' => $i + 2, + ]; + } + } else { + // Moving down: items at 0-indexed [old_pos, new_pos-1] shift up by -1. + for ( $i = $old_position_normalized; $i <= $new_position - 1; $i++ ) { + $pending_menu_order_updates[] = [ + 'ID' => $sorted_item_ids[ $i ], + 'menu_order' => $i, + ]; + } + } + } } } @@ -710,9 +706,25 @@ private function add_or_update_item( $method, $type, $args, $assoc_args ) { } } else { // Apply deferred reordering of other menu items only after a successful update. - foreach ( $pending_menu_order_updates as $update_args ) { - wp_update_post( $update_args ); - } + if ( ! empty( $pending_menu_order_updates ) ) { + global $wpdb; + + $ids_to_update = []; + $case_clauses = ''; + foreach ( $pending_menu_order_updates as $update_args ) { + $item_id = (int) $update_args['ID']; + $ids_to_update[] = $item_id; + $case_clauses .= $wpdb->prepare( ' WHEN %d THEN %d', $item_id, $update_args['menu_order'] ); + } + + $ids_sql = implode( ',', $ids_to_update ); + // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared -- $case_clauses and $ids_sql are constructed from prepared/safe values. + $wpdb->query( "UPDATE {$wpdb->posts} SET menu_order = CASE ID {$case_clauses} END WHERE ID IN ({$ids_sql})" ); + + foreach ( $ids_to_update as $id ) { + clean_post_cache( $id ); + } + } if ( ( 'add' === $method ) && $menu_item_args['menu-item-position'] ) { $this->reorder_menu_items( $menu->term_id, $menu_item_args['menu-item-position'], +1, $result ); From 033baf297c83a672091bc3dc93f31ae163c08cac Mon Sep 17 00:00:00 2001 From: Pascal Birchler Date: Thu, 19 Mar 2026 11:31:14 +0100 Subject: [PATCH 10/11] PHPCS fix --- src/Menu_Item_Command.php | 134 +++++++++++++++++++------------------- 1 file changed, 67 insertions(+), 67 deletions(-) diff --git a/src/Menu_Item_Command.php b/src/Menu_Item_Command.php index a349d545c..c5d91eded 100644 --- a/src/Menu_Item_Command.php +++ b/src/Menu_Item_Command.php @@ -643,54 +643,54 @@ private function add_or_update_item( $method, $type, $args, $assoc_args ) { // Fetch all menu items sorted by their raw menu_order to determine // normalized (1-indexed) ranks, since wp_get_nav_menu_items(ARRAY_A) // normalises menu_order to 1,2,3… which may differ from the raw DB values. - $sorted_item_ids = get_posts( - [ - 'post_type' => 'nav_menu_item', - 'numberposts' => -1, - 'orderby' => 'menu_order', - 'order' => 'ASC', - 'post_status' => 'any', - 'tax_query' => [ - [ - 'taxonomy' => 'nav_menu', - 'field' => 'term_taxonomy_id', - 'terms' => $menu->term_taxonomy_id, - ], - ], - 'fields' => 'ids', - ] - ); - - // Clamp the requested position to the valid range of menu items. - $max_position = count( $sorted_item_ids ); - if ( $max_position > 0 && $new_position > $max_position ) { - // Treat out-of-range positions as "move to end", consistent with core behavior. - $new_position = $max_position; - } - - // Find the 1-indexed normalized rank of the item being moved. - $item_idx = array_search( (string) $menu_item_db_id, $sorted_item_ids, true ); - $old_position_normalized = ( false !== $item_idx ) ? $item_idx + 1 : 0; - - if ( $old_position_normalized > 0 && $new_position !== $old_position_normalized ) { - if ( $new_position < $old_position_normalized ) { - // Moving up: items at 0-indexed [new_pos-1, old_pos-2] shift down by +1. - for ( $i = $new_position - 1; $i <= $old_position_normalized - 2; $i++ ) { - $pending_menu_order_updates[] = [ - 'ID' => $sorted_item_ids[ $i ], - 'menu_order' => $i + 2, - ]; - } - } else { - // Moving down: items at 0-indexed [old_pos, new_pos-1] shift up by -1. - for ( $i = $old_position_normalized; $i <= $new_position - 1; $i++ ) { - $pending_menu_order_updates[] = [ - 'ID' => $sorted_item_ids[ $i ], - 'menu_order' => $i, - ]; - } - } - } + $sorted_item_ids = get_posts( + [ + 'post_type' => 'nav_menu_item', + 'numberposts' => -1, + 'orderby' => 'menu_order', + 'order' => 'ASC', + 'post_status' => 'any', + 'tax_query' => [ + [ + 'taxonomy' => 'nav_menu', + 'field' => 'term_taxonomy_id', + 'terms' => $menu->term_taxonomy_id, + ], + ], + 'fields' => 'ids', + ] + ); + + // Clamp the requested position to the valid range of menu items. + $max_position = count( $sorted_item_ids ); + if ( $max_position > 0 && $new_position > $max_position ) { + // Treat out-of-range positions as "move to end", consistent with core behavior. + $new_position = $max_position; + } + + // Find the 1-indexed normalized rank of the item being moved. + $item_idx = array_search( (string) $menu_item_db_id, $sorted_item_ids, true ); + $old_position_normalized = ( false !== $item_idx ) ? $item_idx + 1 : 0; + + if ( $old_position_normalized > 0 && $new_position !== $old_position_normalized ) { + if ( $new_position < $old_position_normalized ) { + // Moving up: items at 0-indexed [new_pos-1, old_pos-2] shift down by +1. + for ( $i = $new_position - 1; $i <= $old_position_normalized - 2; $i++ ) { + $pending_menu_order_updates[] = [ + 'ID' => $sorted_item_ids[ $i ], + 'menu_order' => $i + 2, + ]; + } + } else { + // Moving down: items at 0-indexed [old_pos, new_pos-1] shift up by -1. + for ( $i = $old_position_normalized; $i <= $new_position - 1; $i++ ) { + $pending_menu_order_updates[] = [ + 'ID' => $sorted_item_ids[ $i ], + 'menu_order' => $i, + ]; + } + } + } } } @@ -706,25 +706,25 @@ private function add_or_update_item( $method, $type, $args, $assoc_args ) { } } else { // Apply deferred reordering of other menu items only after a successful update. - if ( ! empty( $pending_menu_order_updates ) ) { - global $wpdb; - - $ids_to_update = []; - $case_clauses = ''; - foreach ( $pending_menu_order_updates as $update_args ) { - $item_id = (int) $update_args['ID']; - $ids_to_update[] = $item_id; - $case_clauses .= $wpdb->prepare( ' WHEN %d THEN %d', $item_id, $update_args['menu_order'] ); - } - - $ids_sql = implode( ',', $ids_to_update ); - // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared -- $case_clauses and $ids_sql are constructed from prepared/safe values. - $wpdb->query( "UPDATE {$wpdb->posts} SET menu_order = CASE ID {$case_clauses} END WHERE ID IN ({$ids_sql})" ); - - foreach ( $ids_to_update as $id ) { - clean_post_cache( $id ); - } - } + if ( ! empty( $pending_menu_order_updates ) ) { + global $wpdb; + + $ids_to_update = []; + $case_clauses = ''; + foreach ( $pending_menu_order_updates as $update_args ) { + $item_id = (int) $update_args['ID']; + $ids_to_update[] = $item_id; + $case_clauses .= $wpdb->prepare( ' WHEN %d THEN %d', $item_id, $update_args['menu_order'] ); + } + + $ids_sql = implode( ',', $ids_to_update ); + // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared -- $case_clauses and $ids_sql are constructed from prepared/safe values. + $wpdb->query( "UPDATE {$wpdb->posts} SET menu_order = CASE ID {$case_clauses} END WHERE ID IN ({$ids_sql})" ); + + foreach ( $ids_to_update as $id ) { + clean_post_cache( $id ); + } + } if ( ( 'add' === $method ) && $menu_item_args['menu-item-position'] ) { $this->reorder_menu_items( $menu->term_id, $menu_item_args['menu-item-position'], +1, $result ); From 5666a6fdaaa748ed0e37d900a886a6e629abd2b1 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 19 Mar 2026 11:40:12 +0000 Subject: [PATCH 11/11] Fix: normalize IDs to int after get_posts fields=ids to fix strict array_search type mismatch Co-authored-by: swissspidy <841956+swissspidy@users.noreply.github.com> --- src/Menu_Item_Command.php | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/src/Menu_Item_Command.php b/src/Menu_Item_Command.php index c5d91eded..2fa97edfa 100644 --- a/src/Menu_Item_Command.php +++ b/src/Menu_Item_Command.php @@ -661,6 +661,10 @@ private function add_or_update_item( $method, $type, $args, $assoc_args ) { ] ); + // Normalise to integers so that strict comparisons below work regardless of + // whether $wpdb->get_col() returned strings or integers. + $sorted_item_ids = array_map( 'intval', $sorted_item_ids ); + // Clamp the requested position to the valid range of menu items. $max_position = count( $sorted_item_ids ); if ( $max_position > 0 && $new_position > $max_position ) { @@ -669,7 +673,7 @@ private function add_or_update_item( $method, $type, $args, $assoc_args ) { } // Find the 1-indexed normalized rank of the item being moved. - $item_idx = array_search( (string) $menu_item_db_id, $sorted_item_ids, true ); + $item_idx = array_search( (int) $menu_item_db_id, $sorted_item_ids, true ); $old_position_normalized = ( false !== $item_idx ) ? $item_idx + 1 : 0; if ( $old_position_normalized > 0 && $new_position !== $old_position_normalized ) { @@ -718,7 +722,7 @@ private function add_or_update_item( $method, $type, $args, $assoc_args ) { } $ids_sql = implode( ',', $ids_to_update ); - // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared -- $case_clauses and $ids_sql are constructed from prepared/safe values. + // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- $case_clauses and $ids_sql are constructed from prepared/safe integer values. $wpdb->query( "UPDATE {$wpdb->posts} SET menu_order = CASE ID {$case_clauses} END WHERE ID IN ({$ids_sql})" ); foreach ( $ids_to_update as $id ) {