I recently had an issue with dynamic Gutenberg blocks registered in PHP failing to return my block attributes.
The dynamic blocks worked as expected, but after passing them through WPML nothing was being displayed.
After a lot of debugging and going through WPML line by line with xDebug I was able to track it down to a bug in WPML1 that was causing the data to become invalid JSON when the block attributes were being parsed.
Leaving me with no attributes and no blocks on the front-end.
Let’s step back for a minute.
What are Block Attributes
Block attributes are bits of data stored in the block object tree to help render your block in the editor and in the front-end.
A typical block attribute in a dynamic block could be stored to the database like:
<!-- wp:custom-block {"custom-attribute":"some data"} /-->
In this case custom-attribute
is your block attribute.
How are Block Attributes Parsed
When creating dynamic blocks you’ll register the block with the register_block_type()
function in PHP.
When rendering your block with PHP, the register_block_type()
function you’ll add a custom function to render_callback
. That function will get passed your block attributes. Assuming WordPress parses them.
The first step in WordPress parsing those block attributes is do_blocks
. This function is hooked into the the_content
filter with a priority of 92 and parses the HTML comment to read the block attributes. Here’s a look at the function:
// wp-includes/blocks.php
function do_blocks( $content ) {
$blocks = parse_blocks( $content );
$output = '';
foreach ( $blocks as $block ) {
$output .= render_block( $block );
}
// If there are blocks in this content, we shouldn't run wpautop() on it later.
$priority = has_filter( 'the_content', 'wpautop' );
if ( false !== $priority && doing_filter( 'the_content' ) && has_blocks( $content ) ) {
remove_filter( 'the_content', 'wpautop', $priority );
add_filter( 'the_content', '_restore_wpautop_hook', $priority + 1 );
}
return $output;
}
You’ll notice almost immediately this calls out to parse_blocks
. Which actually does the parsing the block from the saved content.
Let’s take a look.
// wp-includes/blocks.php
function parse_blocks( $content ) {
/**
* Filter to allow plugins to replace the server-side block parser
*
* @since 5.0.0
*
* @param string $parser_class Name of block parser class.
*/
$parser_class = apply_filters( 'block_parser_class', 'WP_Block_Parser' );
$parser = new $parser_class();
return $parser->parse( $content );
}
This then kicks us off to the WP_Block_Parser
class which is defined in wp-includes/class-wp-block-parser.php
.
As the comment says, the magic happens on line 412 where preg_match
is used to pull out the block data including attributes.
/*
* aye the magic
* we’re using a single RegExp to tokenize the block comment delimiters
* we're also using a trick here because the only difference between a
* block opener and a block closer is the leading `/` before `wp:` (and
* a closer has no attributes). we can trap them both and process the
* match back in PHP to see which one it was.
*/
$has_match = preg_match(
'/<!--\s+(?P<closer>\/)?wp:(?P<namespace>[a-z][a-z0-9_-]*\/)?(?P<name>[a-z][a-z0-9_-]*)\s+(?P<attrs>{(?:(?:[^}]+|}+(?=})|(?!}\s+\/?-->).)*+)?}\s+)?(?P<void>\/)?-->/s',
$this->document,
$matches,
PREG_OFFSET_CAPTURE,
$this->offset
);
That is a complex RegExp, but the takeaway for this article is coming from that you’ll have $matches['attrs']
, assuming the parser was able to get your attributes.
A bit further down you’ll see a json_decode
function that reads the JSON attributes saved to the block HTML comment in the database and decodes that into a PHP variable to be used in your render_callback
function.
/*
* Fun fact! It’s not trivial in PHP to create “an empty associative array” since all arrays
* are associative arrays. If we use `array()` we get a JSON `[]`
*/
$attrs = $has_attrs
? json_decode( $matches[‘attrs’][0], /* as-associative */ true )
: $this->empty_attrs;
This last piece is where my invisible failure was happening. After WPML saved the translated data it had a bug causing the HTML comment to not contain valid JSON.
The json_decode
function was failing and no attributes were returning without an actual error.
Wrapping Up
To parse attributes in dynamic blocks, WordPress hooks do_blocks
into the the_content
filter.
This calls the WP_Block_Parser
class through parse_blocks
. The block HTML comment is then parsed by a RegExp in the WP_Block_Parser
class.
Your JSON object of saved attributes is then run through json_decode
to turn it into a usable PHP variable and returned to your render_callback
function.
If you ever have an issue with attributes unexpectedly not returning any data to your render_callback
function it could be an issue with invalid JSON saved to the block HTML comment causing json_decode
to fail silently.