* : The name, slug, or term ID for the menu. * * [--fields=] * : Limit the output to specific object fields. * * [--format=] * : Render output in a particular format. * --- * default: table * options: * - table * - csv * - json * - count * - ids * - yaml * --- * * ## AVAILABLE FIELDS * * These fields will be displayed by default for each menu item: * * * db_id * * type * * title * * link * * position * * These fields are optionally available: * * * menu_item_parent * * object_id * * object * * type * * type_label * * target * * attr_title * * description * * classes * * xfn * * ## EXAMPLES * * $ wp menu item list main-menu * +-------+-----------+-------------+---------------------------------+----------+ * | db_id | type | title | link | position | * +-------+-----------+-------------+---------------------------------+----------+ * | 5 | custom | Home | http://example.com | 1 | * | 6 | post_type | Sample Page | http://example.com/sample-page/ | 2 | * +-------+-----------+-------------+---------------------------------+----------+ * * @subcommand list */ public function list_( $args, $assoc_args ) { $items = wp_get_nav_menu_items( $args[0] ); if ( false === $items || is_wp_error( $items ) ) { WP_CLI::error( 'Invalid menu.' ); } // Correct position inconsistency and // protected `url` param in WP-CLI $items = array_map( function ( $item ) { $item->position = $item->menu_order; $item->link = $item->url; return $item; }, $items ); if ( ! empty( $assoc_args['format'] ) && 'ids' === $assoc_args['format'] ) { $items = array_map( function ( $item ) { return $item->db_id; }, $items ); } $formatter = $this->get_formatter( $assoc_args ); $formatter->display_items( $items ); } /** * Adds a post as a menu item. * * ## OPTIONS * * * : The name, slug, or term ID for the menu. * * * : Post ID to add to the menu. * * [--title=] * : Set a custom title for the menu item. * * [--link=<link>] * : Set a custom url for the menu item. * * [--description=<description>] * : Set a custom description for the menu item. * * [--attr-title=<attr-title>] * : Set a custom title attribute for the menu item. * * [--target=<target>] * : Set a custom link target for the menu item. * * [--classes=<classes>] * : Set a custom link classes for the menu item. * * [--position=<position>] * : Specify the position of this menu item. * * [--parent-id=<parent-id>] * : Make this menu item a child of another menu item. * * [--porcelain] * : Output just the new menu item id. * * ## EXAMPLES * * $ wp menu item add-post sidebar-menu 33 --title="Custom Test Post" * Success: Menu item added. * * @subcommand add-post */ public function add_post( $args, $assoc_args ) { $assoc_args['object-id'] = $args[1]; unset( $args[1] ); $post = get_post( $assoc_args['object-id'] ); if ( ! $post ) { WP_CLI::error( 'Invalid post.' ); } $assoc_args['object'] = $post->post_type; $this->add_or_update_item( 'add', 'post_type', $args, $assoc_args ); } /** * Adds a taxonomy term as a menu item. * * ## OPTIONS * * <menu> * : The name, slug, or term ID for the menu. * * <taxonomy> * : Taxonomy of the term to be added. * * <term-id> * : Term ID of the term to be added. * * [--title=<title>] * : Set a custom title for the menu item. * * [--link=<link>] * : Set a custom url for the menu item. * * [--description=<description>] * : Set a custom description for the menu item. * * [--attr-title=<attr-title>] * : Set a custom title attribute for the menu item. * * [--target=<target>] * : Set a custom link target for the menu item. * * [--classes=<classes>] * : Set a custom link classes for the menu item. * * [--position=<position>] * : Specify the position of this menu item. * * [--parent-id=<parent-id>] * : Make this menu item a child of another menu item. * * [--porcelain] * : Output just the new menu item id. * * ## EXAMPLES * * $ wp menu item add-term sidebar-menu post_tag 24 * Success: Menu item added. * * @subcommand add-term */ public function add_term( $args, $assoc_args ) { $assoc_args['object'] = $args[1]; unset( $args[1] ); $assoc_args['object-id'] = $args[2]; unset( $args[2] ); if ( ! get_term_by( 'id', $assoc_args['object-id'], $assoc_args['object'] ) ) { WP_CLI::error( 'Invalid term.' ); } $this->add_or_update_item( 'add', 'taxonomy', $args, $assoc_args ); } /** * Adds a custom menu item. * * ## OPTIONS * * <menu> * : The name, slug, or term ID for the menu. * * <title> * : Title for the link. * * <link> * : Target URL for the link. * * [--description=<description>] * : Set a custom description for the menu item. * * [--attr-title=<attr-title>] * : Set a custom title attribute for the menu item. * * [--target=<target>] * : Set a custom link target for the menu item. * * [--classes=<classes>] * : Set a custom link classes for the menu item. * * [--position=<position>] * : Specify the position of this menu item. * * [--parent-id=<parent-id>] * : Make this menu item a child of another menu item. * * [--porcelain] * : Output just the new menu item id. * * ## EXAMPLES * * $ wp menu item add-custom sidebar-menu Apple http://apple.com * Success: Menu item added. * * @subcommand add-custom */ public function add_custom( $args, $assoc_args ) { $assoc_args['title'] = $args[1]; unset( $args[1] ); $assoc_args['link'] = $args[2]; unset( $args[2] ); $this->add_or_update_item( 'add', 'custom', $args, $assoc_args ); } /** * Updates a menu item. * * ## OPTIONS * * <db-id> * : Database ID for the menu item. * * [--title=<title>] * : Set a custom title for the menu item. * * [--link=<link>] * : Set a custom url for the menu item. * * [--description=<description>] * : Set a custom description for the menu item. * * [--attr-title=<attr-title>] * : Set a custom title attribute for the menu item. * * [--target=<target>] * : Set a custom link target for the menu item. * * [--classes=<classes>] * : Set a custom link classes for the menu item. * * [--position=<position>] * : Specify the position of this menu item. * * [--parent-id=<parent-id>] * : Make this menu item a child of another menu item. * * ## EXAMPLES * * $ wp menu item update 45 --title=WordPress --link='http://wordpress.org' --target=_blank --position=2 * Success: Menu item updated. * * @subcommand update */ public function update( $args, $assoc_args ) { // Shuffle the position of these. $args[1] = $args[0]; $terms = get_the_terms( $args[1], 'nav_menu' ); if ( $terms && ! is_wp_error( $terms ) ) { $args[0] = (int) $terms[0]->term_id; } else { $args[0] = 0; } $type = get_post_meta( $args[1], '_menu_item_type', true ); $this->add_or_update_item( 'update', $type, $args, $assoc_args ); } /** * Deletes one or more items from a menu. * * ## OPTIONS * * <db-id>... * : Database ID for the menu item(s). * * ## EXAMPLES * * $ wp menu item delete 45 * Success: Deleted 1 of 1 menu items. * * @subcommand delete */ public function delete( $args, $assoc_args ) { global $wpdb; $count = 0; $errors = 0; foreach ( $args as $arg ) { $post = get_post( $arg ); $menu_term = get_the_terms( $arg, 'nav_menu' ); $parent_menu_id = (int) get_post_meta( $arg, '_menu_item_menu_item_parent', true ); $result = wp_delete_post( $arg, true ); if ( ! $result ) { WP_CLI::warning( "Couldn't delete menu item {$arg}." ); ++$errors; } else { if ( is_array( $menu_term ) && ! empty( $menu_term ) && $post ) { $this->reorder_menu_items( $menu_term[0]->term_id, $post->menu_order, -1, 0 ); } if ( $parent_menu_id ) { $children = $wpdb->get_results( $wpdb->prepare( "SELECT post_id FROM $wpdb->postmeta WHERE meta_key='_menu_item_menu_item_parent' AND meta_value=%s", (int) $arg ) ); if ( $children ) { $children_query = $wpdb->prepare( "UPDATE $wpdb->postmeta SET meta_value = %d WHERE meta_key = '_menu_item_menu_item_parent' AND meta_value=%s", $parent_menu_id, (int) $arg ); // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared -- $children_query is already prepared above. $wpdb->query( $children_query ); foreach ( $children as $child ) { clean_post_cache( $child ); } } } } // phpcs:ignore Universal.Operators.StrictComparisons.LooseNotEqual -- Will increase count for non existent menu. if ( false != $result ) { ++$count; } } Utils\report_batch_operation_results( 'menu item', 'delete', count( $args ), $count, $errors ); } /** * Worker method to create new items or update existing ones. */ private function add_or_update_item( $method, $type, $args, $assoc_args ) { $menu = $args[0]; $menu_item_db_id = Utils\get_flag_value( $args, 1, 0 ); $menu = wp_get_nav_menu_object( $menu ); if ( ! $menu || is_wp_error( $menu ) ) { WP_CLI::error( 'Invalid menu.' ); } // `url` is protected in WP-CLI, so we use `link` instead $assoc_args['url'] = Utils\get_flag_value( $assoc_args, 'link' ); // Need to persist the menu item data. See https://core.trac.wordpress.org/ticket/28138 if ( 'update' === $method ) { $menu_item_obj = get_post( $menu_item_db_id ); $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; $default_args = [ 'position' => $position, 'title' => $menu_item_obj->title, 'url' => $menu_item_obj->url, 'description' => $menu_item_obj->description, 'object' => $menu_item_obj->object, 'object-id' => $menu_item_obj->object_id, 'parent-id' => $menu_item_obj->menu_item_parent, 'attr-title' => $menu_item_obj->attr_title, 'target' => $menu_item_obj->target, 'classes' => implode( ' ', $menu_item_obj->classes ), // stored in the database as array 'xfn' => $menu_item_obj->xfn, 'status' => $menu_item_obj->post_status, ]; } else { $default_args = [ 'position' => 0, 'title' => '', 'url' => '', 'description' => '', 'object' => '', 'object-id' => 0, 'parent-id' => 0, 'attr-title' => '', 'target' => '', 'classes' => '', 'xfn' => '', // Core oddly defaults to 'draft' for create, // and 'publish' for update // Easiest to always work with publish 'status' => 'publish', ]; } $menu_item_args = []; foreach ( $default_args as $key => $default_value ) { // wp_update_nav_menu_item() has a weird argument prefix $new_key = 'menu-item-' . $key; $menu_item_args[ $new_key ] = Utils\get_flag_value( $assoc_args, $key, $default_value ); } $menu_item_args['menu-item-type'] = $type; $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() ); } elseif ( ! $result ) { if ( 'add' === $method ) { WP_CLI::error( "Couldn't add menu item." ); } elseif ( 'update' === $method ) { WP_CLI::error( "Couldn't update menu item." ); } } else { if ( ( 'add' === $method ) && $menu_item_args['menu-item-position'] ) { $this->reorder_menu_items( $menu->term_id, $menu_item_args['menu-item-position'], +1, $result ); } /** * Set the menu * * wp_update_nav_menu_item() *should* take care of this, but * depends on wp_insert_post()'s "tax_input" argument, which * is ignored if the user can't edit the taxonomy * * @see https://core.trac.wordpress.org/ticket/27113 */ if ( ! is_object_in_term( $result, 'nav_menu', (int) $menu->term_id ) ) { wp_set_object_terms( $result, [ (int) $menu->term_id ], 'nav_menu' ); } if ( 'add' === $method && ! empty( $assoc_args['porcelain'] ) ) { WP_CLI::line( $result ); } elseif ( 'add' === $method ) { WP_CLI::success( 'Menu item added.' ); } elseif ( 'update' === $method ) { WP_CLI::success( 'Menu item updated.' ); } } } /** * Move block of items in one nav_menu up or down by incrementing/decrementing their menu_order field. * Expects the menu items to have proper menu_orders (i.e. doesn't fix errors from previous incorrect operations). * * @param int $menu_id ID of the nav_menu * @param int $min_position minimal menu_order to touch * @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. newly created menu item) * @return int number of rows affected */ private function reorder_menu_items( $menu_id, $min_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`>=%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 ) ); } protected function get_formatter( &$assoc_args ) { return new Formatter( $assoc_args, $this->obj_fields ); } }