Gutenberg: Case study: convert group of file blocks to ACF block

How to convert a block into another block?

Let’s convert this (source block)

<!-- wp:acea/group-content {"type":"_pdf_","allowedBlocks":["core/file"],"max":900} -->
    <div class="_pdf_ block-container">
        <!-- wp:file {"id":920,"href":"http://localhost/files/Screenshot-from-2020-05-23-14-53-28-1.png"} -->
            <div class="wp-block-file"><a href="http://localhost/files/Screenshot-from-2020-05-23-14-53-28-1.png">Screenshot-from-2020-05-23-14-53-28-1</a><a href="http://localhost/files/Screenshot-from-2020-05-23-14-53-28-1.png" class="wp-block-file__button" download>Download</a></div>
        <!-- /wp:file -->

        <!-- wp:file {"id":915,"href":"http://localhost/files/Screenshot-from-2020-05-23-14-53-28.png"} -->
            <div class="wp-block-file"><a href="http://localhost/files/Screenshot-from-2020-05-23-14-53-28.png">Screenshot-from-2020-05-23-14-53-28</a><a href="http://localhost/files/Screenshot-from-2020-05-23-14-53-28.png" class="wp-block-file__button" download>Download</a></div>
        <!-- /wp:file -->
    </div>
<!-- /wp:acea/group-content -->

to this (target block):

<!-- wp:acea/group-content {"type":"cntnt","allowedBlocks":[],"max":900} -->
    <div class="cntnt block-container">
        <!-- wp:acf/files {
            "id":"block_5f3e47cb52c75",
            "name":"acf/files",
            "data":{
                "files_0_file":  915,
                "_files_0_file": "field_5f3e4591bd4e6",
                "files_1_file":  920,
                "_files_1_file": "field_5f3e4591bd4e6",
                "files":2,
                "_files":        "field_5f3e4578bd4e5"
            },
            "mode":"edit"
        } /-->
    </div>
<!-- /wp:acea/group-content -->

Before we can do that, we need to define the template for the target block. I’m going to use some very basic Mustache syntax for the template. Should be self-explanatory:

<!-- wp:acea/group-content {"type":"cntnt","allowedBlocks":[],"max":900} -->
    <div class="cntnt block-container">
        <!-- wp:acf/files {
            "id":"block_5f3e47cb52c75",
            "name":"acf/files",
            "data":{
                {{#files}}
                "files_{{i}}_file":{{id}},
                "_files_{{i}}_file":"field_5f3e4591bd4e6",
                {{/files}}
                "files":{{file_count}},
                "_files":"field_5f3e4578bd4e5"
            },
            "mode":"edit"
        } /-->
    </div>
<!-- /wp:acea/group-content -->

Now, all we need to do is create a function that takes the source block HTML code, converts it to a block, extract the relevant data, inject the data into the template, and return the result. Here it is:

function bub_convert_block($src_block_str, $target_template) {

    $src_block = parse_blocks($src_block_str)[0];

    $files = [];

    foreach ($src_block['innerBlocks'] as $i => $file_block) {
        $files[] = [
            'id'=> $file_block['attrs']['id'],
            'i'=> $i
        ];
    }

    $m = new Mustache_Engine(array('entity_flags' => ENT_QUOTES));
    return $m->render($target_template, [
        'files' => $files,
        'file_count' => count($files),
    ]);

}

Note: if you want to use Mustache_Engine in your own code, you need to include the mustache library. The easiest way to do this is by installing Composer, create a composer.json file like this:

{
    "require": {
        "mustache/mustache": "2.13.0"
    }
}

In the same folder, run this command in the terminal:

composer i

Then add this line at the top of your PHP file:

require __DIR__ . '/vendor/autoload.php';

How to update all blocks of a specific kind inside all posts?

First, let’s define a pattern to search for the block we need. Based on the way WordPress defines block templates, we could use this JSON template:

[
    "acea/group-content", 
    {
        "type":"_pdf_"
    }
]

What this template tells us, is to search for blocks with the name acea/group-content, that have a type attribute set to "_pdf_". We could make the block template more complex, by also including innerBlocks that need to be matched, but let’s keep it simple for now.

Now wouldn’t it be nice if we could write a function that takes a template, like the one above. Then it goes searching for all blocks that match this template, in a given set of posts. And finally, it updates all the found blocks to their brand new version, which in our case is a completely different block?

Let’s define some more functions first:

function bub_replace_block($src_template, $target_template, $post_id) {

    $blocks = bub_get_blocks_by_post_id($post_id);

    foreach($blocks as $i => $block) {
        if (bub_block_is_a_match($block, $src_template)) {
            $blocks_to_insert = bub_convert_block($block, $target_template);
            array_splice($blocks, $i, 1, $blocks_to_insert);
        }
    }

    return bub_update_blocks($post_id, $blocks);
}

function bub_block_is_a_match($block, $template) {

    $name = $template[0];
    $attrs = isset($template[1]) ? $template[1] : [];

    if ($block['blockName'] !== $name) {
        return false;
    }
    foreach ($attrs as $attr_name => $attr_val) {
        if (
            !in_array($attr_name, array_keys($block['attrs'])) ||
            $block['attrs'][$attr_name] != $attr_val
        ) {
            return false;
        }
    }
    return true;
}

function bub_update_blocks($post_id, $blocks) {
    global $post;

    $post = get_post($post_id);
    setup_postdata($post);
    $post->post_content = serialize_blocks($blocks);

    // Update the post into the database
    $success = wp_update_post( $post );

    wp_reset_postdata();
    return $success;
}

Now, with these functions in place, and given an array of post IDs $post_to_update, we can update all the post with the new block version like this:

$posts_to_update = [1,2,3];
$src_template = '["acea/group-content", {"type":"_pdf_"}]';
ob_start();
?>
<!-- wp:acea/group-content {"type":"cntnt","allowedBlocks":[],"max":900} -->
    <div class="cntnt block-container">
        <!-- wp:acf/files {
            "id":"block_5f3e47cb52c75",
            "name":"acf/files",
            "data":{
                {{#files}}
                "files_{{i}}_file":{{id}},
                "_files_{{i}}_file":"field_5f3e4591bd4e6",
                {{/files}}
                "files":{{file_count}},
                "_files":"field_5f3e4578bd4e5"
            },
            "mode":"edit"
        } /-->
    </div>
<!-- /wp:acea/group-content -->
<?php
$target_template = trim(ob_get_clean());

foreach($posts_to_update as $id) {
    bub_replace_block( $src_template, $target_template, $id );
}