Programatically update blocks in a layout

Hi all, I’m hoping someone might be able to help me here. I feel like I’m close but the last thing I have to do is actually update my page with an updated layout from my updated blocks. I have been successful updating a blocks field using a similar solution which is why there is that comment part at the bottom. Hopefully its clear what I’m trying to do, let me know if I can explain anything better!

if ( $newPage->layout()->exists() ) {
  if ( $newPage->layout()->toLayouts()->isNotEmpty() ){
    // There is a layout

    $oldLayoutBlocks = $oldPage->layout()->toLayouts()->toBlocks();
    $newLayoutBlocks = $newPage->layout()->toLayouts()->toBlocks();

    if($newLayoutBlocks->isNotEmpty() ){
      // There are blocks in the layout

      $oldMediaBlocks = $oldLayoutBlocks->filter('type', 'media');
      $newMediaBlocks = $newLayoutBlocks->filter('type', 'media');

      if (Custom\App\App::fieldValuesChanged($oldMediaBlocks->toJson(), $newMediaBlocks->toJson()) ){
        foreach ($newMediaBlocks as $newMediaBlock){
          $old = $newMediaBlock->toArray();
            if ( $newMediaBlock->primaryVideoId()->isNotEmpty() ){
              $data = Custom\App\App::getVideoData($newMediaBlock->primaryVideoId()->value(), $newMediaBlock->primaryVideoResolution()->value());

              $old['content']['primaryVideoThumbnailLink'] = $data['thumbnailLink'];
              $old['content']['primaryVideoLink'] =  $data['fileLink'];
            } else {
              $old['content']['primaryVideoThumbnailLink'] = '';
              $old['content']['primaryVideoLink'] = '';
            }
          $new[] = new Kirby\Cms\Block($old);
        }

        $blocks = new Kirby\Cms\Blocks($new ?? []);

        // $newPage->update([
        //     'primaryBlocks' => json_encode($blocks->toArray()),
        // ]);
      }

    }
  }
}

I don’t think it’s a good idea to first extract the blocks from the layout, because you end up with a collection of blocks that has no reference to the layouts and columns they belonged to.

Here Bastian posted how you can create layouts programmatically:

So I think your best bet would be to actually loop through the layouts and blocks, change what needs to be changed and then to save it all back.

I’m just wondering why all this is even necessary and can’t be done when rendering on the frontend?

Thanks @texnixe. I see what you mean, the reason I separated the blocks is because I only need to change them if they are of a certain type. I thought this way I could leave all the other block types.

That “getVideoData” function gets data from the Vimeo API, this is my solution for saving that data into the block fields in the layouts.

I think maybe your suggestion is going to be that only way to accomplish this so I’ll give it a go!

I think I’m on the right track here but I can’t quite get the part that actually saves the block in the layout. Can anyone advise on this?

if ( $newPage->layout()->exists() ) {
  if ( $newPage->layout()->toLayouts()->isNotEmpty() ){

    $oldLayoutBlocks = $oldPage->layout()->toLayouts()->toBlocks();
    $newLayoutBlocks = $newPage->layout()->toLayouts()->toBlocks();

    if($newLayoutBlocks->isNotEmpty() ){

      $oldMediaBlocks = $oldLayoutBlocks->filter('type', 'media');
      $newMediaBlocks = $newLayoutBlocks->filter('type', 'media');

      if (Custom\App\App::fieldValuesChanged($oldMediaBlocks->toJson(), $newMediaBlocks->toJson()) ){

        foreach ($newPage->layout()->toLayouts() as $layout){
          foreach ($layout->columns() as $column){
            foreach ($column->blocks() as $block){
              if($block->type() === "media"){
                $old = $block->toArray();
                if ( $block->primaryVideoId()->isNotEmpty() ){
                  $data = Custom\App\App::getVideoData($block->primaryVideoId()->value(), $block->primaryVideoResolution()->value());
                  $old['content']['primaryVideoThumbnailLink'] = $data['thumbnailLink'];
                  $old['content']['primaryVideoLink'] =  $data['fileLink'];
                } else {
                  $old['content']['primaryVideoThumbnailLink'] = '';
                  $old['content']['primaryVideoLink'] = '';
                }

                $new[] = new Kirby\Cms\Block($old);

                dump($new);
              }
            }
          }
        }
      }
    }
  }
}

@texnixe can you advise any further on how I can do this? I’m not the best with my PHP but I feel like I’m so close. Here’s what I’ve got at the moment, it’s been lots of trail and error. Hopefully it’s not completely wrong? I’d be grateful for any advice!

if ( $newPage->layout()->exists() ) {
  if ( $newPage->layout()->toLayouts()->isNotEmpty() ){
    $oldLayoutBlocks = $oldPage->layout()->toLayouts()->toBlocks();
    $newLayoutBlocks = $newPage->layout()->toLayouts()->toBlocks();

    if($newLayoutBlocks->isNotEmpty() ){
      $oldMediaBlocks = $oldLayoutBlocks->filter('type', 'media');
      $newMediaBlocks = $newLayoutBlocks->filter('type', 'media');

      if (Custom\App\App::fieldValuesChanged($oldMediaBlocks->toJson(), $newMediaBlocks->toJson()) ){
        foreach ($newPage->layout()->toLayouts() as $layout){
          foreach ($layout->columns() as $column){
            $blocksArray = array();
            foreach ($column->blocks() as $block){
              if($block->type() === "media"){
                $old = $block->toArray();
                if ( $block->primaryVideoId()->isNotEmpty() ){
                  $data = Custom\App\App::getVideoData($block->primaryVideoId()->value(), $block->primaryVideoResolution()->value());
                  $old['content']['primaryVideoThumbnailLink'] = $data['thumbnailLink'];
                  $old['content']['primaryVideoLink'] =  $data['fileLink'];
                } else {
                  $old['content']['primaryVideoThumbnailLink'] = '';
                  $old['content']['primaryVideoLink'] = '';
                }
                $new[] = new Kirby\Cms\Block($old);
                array_push($blocksArray, $new);
              } else {
                $new[] = new Kirby\Cms\Block($block->toArray());
                array_push($blocksArray, $new);
              }
            }
            $newBlocks = new Kirby\Cms\Blocks($blocksArray ?? []);
            $newColumn = new Kirby\Cms\LayoutColumn( $newBlocks->toArray() );
            dump($newColumn);
            dump($column);
          }
        }
      }
    }
  }
}

Still trying to work this out, here is my latest code. @bastianallgeier is this nearly right?

if (Custom\App\App::fieldValuesChanged($oldMediaBlocks->toJson(), $newMediaBlocks->toJson()) ){
  $layoutArray = array();
  foreach ($newPage->layout()->toLayouts() as $layout){
    $columnsArray = array();
    foreach ($layout->columns() as $column){
      $blocksArray = array();
      foreach ($column->blocks() as $block){
        if($block->type() === "media"){
          $old = $block->toArray();
          if ( $block->primaryVideoId()->isNotEmpty() ){
            $data = Custom\App\App::getVideoData($block->primaryVideoId()->value(), $block->primaryVideoResolution()->value());

            $old['content']['primaryVideoThumbnailLink'] = $data['thumbnailLink'];
            $old['content']['primaryVideoLink'] =  $data['fileLink'];
          } else {
            $old['content']['primaryVideoThumbnailLink'] = '';
            $old['content']['primaryVideoLink'] = '';
          }
          $new[] = new Kirby\Cms\Block($old);
          array_push($blocksArray, $new[0]);
        } else {
          $new[] = new Kirby\Cms\Block($block->toArray());
          array_push($blocksArray, $new[0]);
        }
      }
      $newBlocks = new Kirby\Cms\Blocks($blocksArray ?? []);
      $newColumn = new Kirby\Cms\LayoutColumn(
        [
          'blocks' => $newBlocks->toArray(),
        ]
      );
      array_push($columnsArray, $newColumn);
    }
    $newLayoutColumns = new Kirby\Cms\LayoutColumns( $columnsArray );
    $newLayout = Kirby\Cms\Layout::factory(
      [
        'columns' => $newLayoutColumns->toArray(),
      ]
    );
    array_push($layoutArray, $newLayout);
  }
  $newLayouts = new Kirby\Cms\Layouts( $layoutArray );

  // $newPage->update([
  //   'layout' => json_encode($nothing),
  // ]);
}
1 Like

I believe I got closer to working this out but in the end I gave up. I am still interested if it’s possible. Does anyone know if I’m even close?

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.