There are multiple reasons why block attributes need to be changed. Maybe because their requirements changed, or maybe because one didn’t thought about it in the first place. This guide will show you how to migrate block attributes of existing blocks.

Introduction

Block attributes store configuration for a particular block instance in the block editor. They are stored directly at the post and within the post content field in the database. Replacing it via MySQL may be possible (depending on your use case), but is not recommended.

In my case here, the attribute is a breakpoint for a specific block, who needs to be changed from an integer to a string to change the attribute field from a regular number input field to an enhanced unit control. This will then store the value, e.g. 768, as 768px.

Note: This migration does only work for block attributes not having a selector or source defined, since those values are handled differently (they’re directly part of the block’s HTML).

This article also only covers the actual migration process of existing attribute values in the database, not the React part to change the attributes and controls or the migration on your block. The latter should not be required after running the migration below.

The script

My script is built to run via WP-CLI as eval-script, which allows to run any third-party PHP script in context of the WordPress instance. Also, since I run it inside a multisite, I iterate over each site.

What it actually does is getting all posts (in my case pages, posts and synced patterns) and parsing the block content to an associative array with blocks. This makes all attributes handleable via PHP. Then, I change the values of my attributes according to my requirements. Since I parsed the blocks as array before, I need to transform them to a string again to update the changed value again. This is what serialize_blocks is for.

The script runs in a loop, each iteration processing 5 posts to keep the memory usage low.

Afterwards, it does the same with block widgets. This, however, is much less complex since all widget blocks are inside a single option widget_block, so there’s no loop needed here.

The code

<?php
$posts_args = [
	'ignore_sticky_posts' => true,
	'no_found_rows' => true,
	'offset' => 0,
	'post_type' => [
		'page',
		'post',
		'wp_block',
	],
	'update_post_meta_cache' => false,
	'update_post_term_cache' => false,
];
$sites = \get_sites( [
	'fields' => 'ids',
	'number' => 1000000,
] );

foreach ( $sites as $site_id ) {
	\switch_to_blog( $site_id );
	
	echo 'Processing ' . \home_url() . PHP_EOL;
	
	$posts_args['offset'] = 0;
	$_posts = \get_posts( $posts_args );
	
	while ( \count( $_posts ) ) {
		foreach ( $_posts as $_post ) {
			if ( ! \has_block( 'core/columns', $_post->ID ) ) {
				continue;
			}
			
			$blocks = \parse_blocks( $_post->post_content );
			$updated_blocks = migrate_column_attributes( $blocks, $_post->post_name );
			
			if ( $blocks !== $updated_blocks ) {
				$_post->post_content = \serialize_blocks( $updated_blocks );
				
				\wp_update_post( $_post );
			}
		}
		$posts_args['offset'] += 5;
		$_posts = \get_posts( $posts_args );
	}
	
	$widgets = \get_option( 'widget_block' );
	$has_changed_widgets = false;
	
	foreach ( $widgets as $widget_id => $widget ) {
		if ( \is_int( $widget ) ) {
			continue;
		}
		
		$blocks = \parse_blocks( $widget['content'] );
		$updated_blocks = rh_migrate_column_attributes( $blocks, 'widget ' . $widget_id );
		
		if ( $blocks !== $updated_blocks ) {
			$has_changed_widgets = true;
			$widgets[ $widget_id ]['content'] = \serialize_blocks( $updated_blocks );
		}
	}
	
	if ( $has_changed_widgets ) {
		\update_option( 'widget_block', $widgets );
	}
	
	\restore_current_blog();
}

function migrate_column_attributes( array $blocks, string $identifier ): array {
	foreach ( $blocks as &$block ) {
		if ( ! empty( $block['innerBlocks'] ) ) {
			$block['innerBlocks'] = rh_migrate_column_attributes( $block['innerBlocks'], $identifier );
		}
		
		if ( empty( $block['blockName'] ) || $block['blockName'] !== 'core/columns' ) {
			continue;
		}
		
		$old = [
			'default' => null,
			'large' => null,
		];
		
		if ( ! empty( $block['attrs']['breakpoint'] ) && \is_int( $block['attrs']['breakpoint'] ) ) {
			$old['default'] = $block['attrs']['breakpoint'];
			$block['attrs']['breakpoint'] = (string) $block['attrs']['breakpoint'];
		}
		
		if ( ! empty( $block['attrs']['breakpointLarge'] ) && \is_int( $block['attrs']['breakpointLarge'] ) ) {
			$old['large'] = $block['attrs']['breakpointLarge'];
			$block['attrs']['breakpointLarge'] = (string) $block['attrs']['breakpointLarge'];
		}
		
		if ( ! empty( $old['default'] ) || ! empty( $default['large'] ) ) {
			echo \home_url() . ' - Migrated ' . $identifier . ': ' . ( ! empty( $old['default'] ) ? 'default ' . $old['default'] . '/"' . $block['attrs']['breakpoint'] . '"; ' : '' ) . ( ! empty( $old['large'] ) ? 'large ' . $old['large'] . '/"' . $block['attrs']['breakpointLarge'] . '"' : '' ) . PHP_EOL;
		}
	}
	
	return $blocks;
}

Code language: PHP (php)

Afterwards, all attributes are migrated in the database and you can push your updated React code (if needed) to production. Since updating uses the builtin WordPress core functions, updating caches etc. is already handled automatically.

Leave a Reply

Your email address will not be published. Required fields are marked *