diff --git a/src/wp-includes/class-wp-customize-nav-menus.php b/src/wp-includes/class-wp-customize-nav-menus.php index 3ff1961e3e..b01cf73758 100644 --- a/src/wp-includes/class-wp-customize-nav-menus.php +++ b/src/wp-includes/class-wp-customize-nav-menus.php @@ -496,7 +496,12 @@ final class WP_Customize_Nav_Menus { // Create a setting for each menu item (which doesn't actually manage data, currently). $menu_item_setting_id = 'nav_menu_item[' . $item->ID . ']'; - $this->manager->add_setting( new WP_Customize_Nav_Menu_Item_Setting( $this->manager, $menu_item_setting_id ) ); + + $value = (array) $item; + $value['nav_menu_term_id'] = $menu_id; + $this->manager->add_setting( new WP_Customize_Nav_Menu_Item_Setting( $this->manager, $menu_item_setting_id, array( + 'value' => $value, + ) ) ); // Create a control for each menu item. $this->manager->add_control( new WP_Customize_Nav_Menu_Item_Control( $this->manager, $menu_item_setting_id, array( diff --git a/src/wp-includes/class-wp-customize-setting.php b/src/wp-includes/class-wp-customize-setting.php index 7f14187fc7..5b63822c5e 100644 --- a/src/wp-includes/class-wp-customize-setting.php +++ b/src/wp-includes/class-wp-customize-setting.php @@ -701,14 +701,21 @@ class WP_Customize_Nav_Menu_Item_Setting extends WP_Customize_Setting { * * A negative value represents a placeholder ID for a new menu not yet saved. * - * @todo Should this be $db_id, and also use this for WP_Customize_Nav_Menu_Setting::$term_id - * * @since 4.3.0 * @access public * @var int */ public $post_id; + /** + * Storage of pre-setup menu item to prevent wasted calls to wp_setup_nav_menu_item(). + * + * @since 4.3.0 + * @access protected + * @var array + */ + protected $value; + /** * Previous (placeholder) post ID used before creating a new menu item. * @@ -806,11 +813,34 @@ class WP_Customize_Nav_Menu_Item_Setting extends WP_Customize_Setting { } $this->post_id = intval( $matches['id'] ); - - $menu = $this->value(); - $this->original_nav_menu_term_id = $menu['nav_menu_term_id']; + add_action( 'wp_update_nav_menu_item', array( $this, 'flush_cached_value' ), 10, 2 ); parent::__construct( $manager, $id, $args ); + + // Ensure that an initially-supplied value is valid. + if ( isset( $this->value ) ) { + $this->populate_value(); + foreach ( array_diff( array_keys( $this->default ), array_keys( $this->value ) ) as $missing ) { + throw new Exception( "Supplied nav_menu_item value missing property: $missing" ); + } + } + + } + + /** + * Clear the cached value when this nav menu item is updated. + * + * @since 4.3.0 + * @access public + * + * @param int $menu_id The term ID for the menu. + * @param int $menu_item_id The post ID for the menu item. + */ + public function flush_cached_value( $menu_id, $menu_item_id ) { + unset( $menu_id ); + if ( $menu_item_id === $this->post_id ) { + $this->value = null; + } } /** @@ -821,7 +851,7 @@ class WP_Customize_Nav_Menu_Item_Setting extends WP_Customize_Setting { * * @see wp_setup_nav_menu_item() * - * @return array Instance data. + * @return array|false Instance data array, or false if the item is marked for deletion. */ public function value() { if ( $this->is_previewed && $this->_previewed_blog_id === get_current_blog_id() ) { @@ -833,6 +863,8 @@ class WP_Customize_Nav_Menu_Item_Setting extends WP_Customize_Setting { } else { $value = $post_value; } + } else if ( isset( $this->value ) ) { + $value = $this->value; } else { $value = false; @@ -840,54 +872,109 @@ class WP_Customize_Nav_Menu_Item_Setting extends WP_Customize_Setting { if ( $this->post_id > 0 ) { $post = get_post( $this->post_id ); if ( $post && self::POST_TYPE === $post->post_type ) { - $item = wp_setup_nav_menu_item( $post ); - $value = wp_array_slice_assoc( - (array) $item, - array_keys( $this->default ) - ); - $value['position'] = $item->menu_order; - $value['status'] = $item->post_status; - $value['original_title'] = ''; - - $menus = wp_get_post_terms( $post->ID, WP_Customize_Nav_Menu_Setting::TAXONOMY, array( - 'fields' => 'ids', - ) ); - - if ( ! empty( $menus ) ) { - $value['nav_menu_term_id'] = array_shift( $menus ); - } else { - $value['nav_menu_term_id'] = 0; - } - - if ( 'post_type' === $value['type'] ) { - $original_title = get_the_title( $value['object_id'] ); - } elseif ( 'taxonomy' === $value['type'] ) { - $original_title = get_term_field( 'name', $value['object_id'], $value['object'], 'raw' ); - if ( is_wp_error( $original_title ) ) { - $original_title = ''; - } - } - - if ( ! empty( $original_title ) ) { - $value['original_title'] = html_entity_decode( $original_title, ENT_QUOTES, get_bloginfo( 'charset' ) ); - } + $value = (array) wp_setup_nav_menu_item( $post ); } } if ( ! is_array( $value ) ) { $value = $this->default; } - } - if ( is_array( $value ) ) { - foreach ( array( 'object_id', 'menu_item_parent', 'nav_menu_term_id' ) as $key ) { - $value[ $key ] = intval( $value[ $key ] ); - } + // Cache the value for future calls to avoid having to re-call wp_setup_nav_menu_item(). + $this->value = $value; + $this->populate_value(); + $value = $this->value; } return $value; } + /** + * Ensure that the value is fully populated with the necessary properties. + * + * Translates some properties added by wp_setup_nav_menu_item() and removes others. + * + * @since 4.3.0 + * @access protected + * + * @see WP_Customize_Nav_Menu_Item_Setting::value() + */ + protected function populate_value() { + if ( ! is_array( $this->value ) ) { + return; + } + + if ( isset( $this->value['menu_order'] ) ) { + $this->value['position'] = $this->value['menu_order']; + unset( $this->value['menu_order'] ); + } + if ( isset( $this->value['post_status'] ) ) { + $this->value['status'] = $this->value['post_status']; + unset( $this->value['post_status'] ); + } + + if ( ! isset( $this->value['original_title'] ) ) { + $original_title = ''; + if ( 'post_type' === $this->value['type'] ) { + $original_title = get_the_title( $this->value['object_id'] ); + } elseif ( 'taxonomy' === $this->value['type'] ) { + $original_title = get_term_field( 'name', $this->value['object_id'], $this->value['object'], 'raw' ); + if ( is_wp_error( $original_title ) ) { + $original_title = ''; + } + } + $this->value['original_title'] = html_entity_decode( $original_title, ENT_QUOTES, get_bloginfo( 'charset' ) ); + } + + if ( ! isset( $this->value['nav_menu_term_id'] ) && $this->post_id > 0 ) { + $menus = wp_get_post_terms( $this->post_id, WP_Customize_Nav_Menu_Setting::TAXONOMY, array( + 'fields' => 'ids', + ) ); + if ( ! empty( $menus ) ) { + $this->value['nav_menu_term_id'] = array_shift( $menus ); + } else { + $this->value['nav_menu_term_id'] = 0; + } + } + + foreach ( array( 'object_id', 'menu_item_parent', 'nav_menu_term_id' ) as $key ) { + if ( ! is_int( $this->value[ $key ] ) ) { + $this->value[ $key ] = intval( $this->value[ $key ] ); + } + } + + // Remove remaining properties available on a setup nav_menu_item post object which aren't relevant to the setting value. + $irrelevant_properties = array( + 'ID', + 'comment_count', + 'comment_status', + 'db_id', + 'filter', + 'guid', + 'ping_status', + 'pinged', + 'post_author', + 'post_content', + 'post_content_filtered', + 'post_date', + 'post_date_gmt', + 'post_excerpt', + 'post_mime_type', + 'post_modified', + 'post_modified_gmt', + 'post_name', + 'post_parent', + 'post_password', + 'post_title', + 'post_type', + 'to_ping', + 'type_label', + ); + foreach ( $irrelevant_properties as $property ) { + unset( $this->value[ $property ] ); + } + } + /** * Handle previewing the setting. * @@ -1043,16 +1130,21 @@ class WP_Customize_Nav_Menu_Item_Setting extends WP_Customize_Setting { $item->menu_order = $item->position; unset( $item->position ); - $item->post_author = get_current_user_id(); - if ( $item->title ) { $item->post_title = $item->title; } $item->ID = $this->post_id; + $item->db_id = $this->post_id; $post = new WP_Post( (object) $item ); - $post = wp_setup_nav_menu_item( $post ); + if ( empty( $post->post_author ) ) { + $post->post_author = get_current_user_id(); + } + + if ( ! isset( $post->type_label ) ) { + $post->type_label = null; + } return $post; } @@ -1160,6 +1252,9 @@ class WP_Customize_Nav_Menu_Item_Setting extends WP_Customize_Setting { $is_placeholder = ( $this->post_id < 0 ); $is_delete = ( false === $value ); + // Update the cached value. + $this->value = $value; + add_filter( 'customize_save_response', array( $this, 'amend_customize_save_response' ) ); if ( $is_delete ) { diff --git a/src/wp-includes/nav-menu.php b/src/wp-includes/nav-menu.php index 16213f1ce5..9061acedd1 100644 --- a/src/wp-includes/nav-menu.php +++ b/src/wp-includes/nav-menu.php @@ -658,20 +658,23 @@ function wp_get_nav_menu_items( $menu, $args = array() ) { * Decorates a menu item object with the shared navigation menu item properties. * * Properties: - * - db_id: The DB ID of this item as a nav_menu_item object, if it exists (0 if it doesn't exist). - * - object_id: The DB ID of the original object this menu item represents, e.g. ID for posts and term_id for categories. - * - type: The family of objects originally represented, such as "post_type" or "taxonomy." - * - object: The type of object originally represented, such as "category," "post", or "attachment." - * - type_label: The singular label used to describe this type of menu item. - * - post_parent: The DB ID of the original object's parent object, if any (0 otherwise). - * - menu_item_parent: The DB ID of the nav_menu_item that is this item's menu parent, if any. 0 otherwise. - * - url: The URL to which this menu item points. - * - title: The title of this menu item. - * - target: The target attribute of the link element for this menu item. - * - attr_title: The title attribute of the link element for this menu item. - * - classes: The array of class attribute values for the link element of this menu item. - * - xfn: The XFN relationship expressed in the link of this menu item. - * - description: The description of this menu item. + * - ID: The term_id if the menu item represents a taxonomy term. + * - attr_title: The title attribute of the link element for this menu item. + * - classes: The array of class attribute values for the link element of this menu item. + * - db_id: The DB ID of this item as a nav_menu_item object, if it exists (0 if it doesn't exist). + * - description: The description of this menu item. + * - menu_item_parent: The DB ID of the nav_menu_item that is this item's menu parent, if any. 0 otherwise. + * - object: The type of object originally represented, such as "category," "post", or "attachment." + * - object_id: The DB ID of the original object this menu item represents, e.g. ID for posts and term_id for categories. + * - post_parent: The DB ID of the original object's parent object, if any (0 otherwise). + * - post_title: A "no title" label if menu item represents a post that lacks a title. + * - target: The target attribute of the link element for this menu item. + * - title: The title of this menu item. + * - type: The family of objects originally represented, such as "post_type" or "taxonomy." + * - type_label: The singular label used to describe this type of menu item. + * - url: The URL to which this menu item points. + * - xfn: The XFN relationship expressed in the link of this menu item. + * - _invalid: Whether the menu item represents an object that no longer exists. * * @since 3.0.0 *