Programatically update blocks in a layout

Hi @Moucky and super thanks for this thread and sharing your WIP code! It helped me immensely, and in fact I managed to do something similar for my own use case.

What I needed to do was to replace a bunch of HTML inside a builder field with different type of blocks, some of which are custom blocks of type column (GitHub - youngcut/kirby-column-blocks: Use columns in block fields based on the layout field.). I had to:

  • loop over all blocks and update type text blocks, as well as, loop over sub-blocks inside columns block and update possible text block inside there
  • reconstruct the whole builder block data structure, including the columns type block
  • update the builder field of the page

I got really stuck trying to understand how to put back the custom columns block, which uses the layout feature. At the end, it was thanks to your code that I discovered the Kirby\Cms\LayoutColum method. After that I realised I had to wrap the layout object inside a new block and all worked together. Following my code, in the hope it would be useful to you and anybody else here in the forum.

$updatedBlocks = [];

foreach($blocks as $block) {

    $blockType = $block->type();

    if ($blockType === 'columns') {

        // we have one layout per block, no need to loop over
        $layout = $block->layout()->toLayouts()->first();

        $columnsNew = [];
        foreach($layout->columns() as $column) {
            // we need to:
            // - parse the layout blocks
            // - reconstruct the layout with updated blocks
            // - convert it back to a layout object

            $subblocks = $column->blocks();
            $updatedSubblocks = parseBlocks($subblocks, $client, 'layout');
            $subblocksNew = new Kirby\Cms\Blocks($updatedSubblocks);

            $columnNew = new Kirby\Cms\LayoutColumn(
                [
                    'blocks' => $subblocksNew->toArray(),
                    'width' => $column->width(),
                ]
            );

            array_push($columnsNew, $columnNew);
        };

        $layoutColumnsNew = new Kirby\Cms\LayoutColumns($columnsNew);
        
        $layoutNew = Kirby\Cms\Layout::factory([
            'columns' => $layoutColumnsNew->toArray(),
        ]);

        $layoutsNew = new Kirby\Cms\Layouts([$layoutNew]);

        // -- update block
        $blockLayoutUpdated = [
            'content' => [
                'layout' => $layoutsNew->toArray(),
            ],
            'type' => 'columns',
        ];

        $blockLayoutNew = new Kirby\Cms\Block($blockLayoutUpdated);
        array_push($updatedBlocks, $blockLayoutNew);

    } else if ($blockType === 'text') {

        // custom code to update block-text
       // [...]
        // -- update block
        $blockUpdated = [
            'content' => [
                'text' => $text_new,
                'footnotes' => $footnotes_new,
            ],
            'type' => $block->type(),
        ];

        $blockUpdated = new Kirby\Cms\Block($blockUpdated);
        array_push($updatedBlocks, $blockUpdated);

    } else {
        array_push($updatedBlocks, $block);
    }

}; // -- end blocks foreach
  • parseBlocks() is the function you see above, which is used in a recursive manner to map over the blocks inside the columns block (each columns block has x-number of columns, each with a block)
  • the important steps to reconstruct a block with a layout / columns inside, are:
    • new Kirby\Cms\LayoutColumn()
    • new Kirby\Cms\LayoutColumns()
    • Kirby\Cms\Layout::factory()
    • new Kirby\Cms\Layouts()
    • check the code snippet above for more details, arguments, etc

One you get the data in the $updatedBlocks array, here’s how I updated the builder field:

$blocksNew = new Kirby\Cms\Blocks($updatedBlocks);

// -- write to file
kirby()->impersonate('kirby');
$newPage->update([
  'builder' => json_encode($blocksNew->toArray()),
]);

Tested with Kirby 7.4 and 8.1.

1 Like