Skip to content

Commit

Permalink
Migrate classic menus to block-based menus on theme switch (#36255)
Browse files Browse the repository at this point in the history
* Migrate classic menus to block-based menus on theme switch

* Add more comments

* Short circuit if switching to a theme that does not support FSE

* Preserve the menu name on migration

* Replace WP_Query with wpdb->get_results

* Adjust gutenberg_parse_blocks_from_menu_items to make use of innerContent

* Lint

* Replace return with continue

* Code style: Assign $mapping after $locations

* Update lib/navigation.php

* Update lib/navigation.php

* Rename $mapping to $area_mapping

* Rename $pretend_old_theme to $get_old_theme_stylesheet

* Explain why custom SQL is used instead of WP_Query

* Use post_name instead of MD5 matching

* Update the comment, remove global $wpdb

* Only convert classic menus to blocks when the matching post wasn't found

* Don't remove PHP functions from the navigation block, instead introduce new ones

* Add a comment to explain how some functions are pasted

* Use "Classic menu" as prefix for the migrated post
  • Loading branch information
adamziel authored Nov 5, 2021
1 parent a880e84 commit d0c695c
Showing 1 changed file with 194 additions and 0 deletions.
194 changes: 194 additions & 0 deletions lib/navigation.php
Original file line number Diff line number Diff line change
Expand Up @@ -572,3 +572,197 @@ function gutenberg_get_navigation_areas_paths_to_preload() {
}
return $paths;
}

/**
* Migrates classic menus to block-based menus on theme switch.
*
* @param string $new_name Name of the new theme.
* @param WP_Theme $new_theme New theme.
* @param WP_Theme $old_theme Old theme.
* @see switch_theme WordPress action.
*/
function gutenberg_migrate_nav_on_theme_switch( $new_name, $new_theme, $old_theme ) {
// Do nothing when switching to a theme that does not support site editor.
if ( ! gutenberg_experimental_is_site_editor_available() ) {
return;
}

// get_nav_menu_locations() calls get_theme_mod() which depends on the stylesheet option.
// At the same time, switch_theme runs only after the stylesheet option was updated to $new_theme.
// To retrieve theme mods of the old theme, the getter is hooked to get_option( 'stylesheet' ) so that we
// get the old theme, which causes the get_nav_menu_locations to get the locations of the old theme.
$get_old_theme_stylesheet = function() use ( $old_theme ) {
return $old_theme->get_stylesheet();
};
add_filter( 'option_stylesheet', $get_old_theme_stylesheet );

$locations = get_nav_menu_locations();
$area_mapping = get_option( 'fse_navigation_areas', array() );

foreach ( $locations as $location_name => $menu_id ) {
// Get the menu from the location, skipping if there is no
// menu or there was an error.
$menu = wp_get_nav_menu_object( $menu_id );
if ( ! $menu || is_wp_error( $menu ) ) {
continue;
}

$menu_items = gutenberg_global_get_menu_items_at_location( $location_name );
if ( empty( $menu_items ) ) {
continue;
}

$post_name = 'classic_menu_' . $menu_id;
$post_status = 'publish';

// Get or create to avoid creating too many wp_navigation posts.
$query = new WP_Query;
$matching_posts = $query->query(
array(
'name' => $post_name,
'post_status' => $post_status,
'post_type' => 'wp_navigation',
'posts_per_page' => 1,
)
);

if ( count( $matching_posts ) ) {
$navigation_post_id = $matching_posts[0]->ID;
} else {
$menu_items_by_parent_id = gutenberg_global_sort_menu_items_by_parent_id( $menu_items );
$parsed_blocks = gutenberg_global_parse_blocks_from_menu_items( $menu_items_by_parent_id[0], $menu_items_by_parent_id );
$post_data = array(
'post_type' => 'wp_navigation',
'post_title' => sprintf(
/* translators: %s: the name of the menu, e.g. "Main Menu". */
__( 'Classic menu: %s', 'gutenberg' ),
$menu->name
),
'post_name' => $post_name,
'post_content' => serialize_blocks( $parsed_blocks ),
'post_status' => $post_status,
);
$navigation_post_id = wp_insert_post( $post_data );
}

$area_mapping[ $location_name ] = $navigation_post_id;
}
remove_filter( 'option_stylesheet', $get_old_theme_stylesheet );

update_option( 'fse_navigation_areas', $area_mapping );
}

add_action( 'switch_theme', 'gutenberg_migrate_nav_on_theme_switch', 200, 3 );

// The functions below are copied over from packages/block-library/src/navigation/index.php
// Let's figure out a better way of managing these global PHP dependencies.

/**
* Returns the menu items for a WordPress menu location.
*
* @param string $location The menu location.
* @return array Menu items for the location.
*/
function gutenberg_global_get_menu_items_at_location( $location ) {
if ( empty( $location ) ) {
return;
}

// Build menu data. The following approximates the code in
// `wp_nav_menu()` and `gutenberg_output_block_nav_menu`.

// Find the location in the list of locations, returning early if the
// location can't be found.
$locations = get_nav_menu_locations();
if ( ! isset( $locations[ $location ] ) ) {
return;
}

// Get the menu from the location, returning early if there is no
// menu or there was an error.
$menu = wp_get_nav_menu_object( $locations[ $location ] );
if ( ! $menu || is_wp_error( $menu ) ) {
return;
}

$menu_items = wp_get_nav_menu_items( $menu->term_id, array( 'update_post_term_cache' => false ) );
_wp_menu_item_classes_by_context( $menu_items );

return $menu_items;
}


/**
* Sorts a standard array of menu items into a nested structure keyed by the
* id of the parent menu.
*
* @param array $menu_items Menu items to sort.
* @return array An array keyed by the id of the parent menu where each element
* is an array of menu items that belong to that parent.
*/
function gutenberg_global_sort_menu_items_by_parent_id( $menu_items ) {
$sorted_menu_items = array();
foreach ( (array) $menu_items as $menu_item ) {
$sorted_menu_items[ $menu_item->menu_order ] = $menu_item;
}
unset( $menu_items, $menu_item );

$menu_items_by_parent_id = array();
foreach ( $sorted_menu_items as $menu_item ) {
$menu_items_by_parent_id[ $menu_item->menu_item_parent ][] = $menu_item;
}

return $menu_items_by_parent_id;
}

/**
* Turns menu item data into a nested array of parsed blocks
*
* @param array $menu_items An array of menu items that represent
* an individual level of a menu.
* @param array $menu_items_by_parent_id An array keyed by the id of the
* parent menu where each element is an
* array of menu items that belong to
* that parent.
* @return array An array of parsed block data.
*/
function gutenberg_global_parse_blocks_from_menu_items( $menu_items, $menu_items_by_parent_id ) {
if ( empty( $menu_items ) ) {
return array();
}

$blocks = array();

foreach ( $menu_items as $menu_item ) {
$class_name = ! empty( $menu_item->classes ) ? implode( ' ', (array) $menu_item->classes ) : null;
$id = ( null !== $menu_item->object_id && 'custom' !== $menu_item->object ) ? $menu_item->object_id : null;
$opens_in_new_tab = null !== $menu_item->target && '_blank' === $menu_item->target;
$rel = ( null !== $menu_item->xfn && '' !== $menu_item->xfn ) ? $menu_item->xfn : null;
$kind = null !== $menu_item->type ? str_replace( '_', '-', $menu_item->type ) : 'custom';

$block = array(
'blockName' => isset( $menu_items_by_parent_id[ $menu_item->ID ] ) ? 'core/navigation-submenu' : 'core/navigation-link',
'attrs' => array(
'className' => $class_name,
'description' => $menu_item->description,
'id' => $id,
'kind' => $kind,
'label' => $menu_item->title,
'opensInNewTab' => $opens_in_new_tab,
'rel' => $rel,
'title' => $menu_item->attr_title,
'type' => $menu_item->object,
'url' => $menu_item->url,
),
);

$block['innerBlocks'] = isset( $menu_items_by_parent_id[ $menu_item->ID ] )
? gutenberg_global_parse_blocks_from_menu_items( $menu_items_by_parent_id[ $menu_item->ID ], $menu_items_by_parent_id )
: array();
$block['innerContent'] = array_map( 'serialize_block', $block['innerBlocks'] );

$blocks[] = $block;
}

return $blocks;
}

0 comments on commit d0c695c

Please sign in to comment.