How WordPress Parses Dynamic Block Attributes

Reading Time: 3 minutes

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.

  1. They fixed it pretty quickly after I reported it.
  2. Inside wp-includes/default-filters.php

Pin It on Pinterest