Drupal 11: Programmatically Change A Layout Paragraphs Layout

The Layout Paragraphs module is a great way of combining the flexibility of the layout system with the content component sytem of the Paragraphs module. Using this module you can set up a Paragraph that can understand different layouts and then inject Paragraphs into that layout, all within the confines of a single field.

What this means is that you users can build the layout they want within the edit pages of your Drupal site, without having to guess where Paragraphs will end up in the final site. It makes the site a little easier to edit and means that there should be less previewing of pages before publishing.

When working on a recent project I found that layout Paragraphs was in use, which wasn't a problem. The problem was that the site was quite simple, but had 12 different layouts to pick from. As a consequence, the pages consisted of a variety of different layouts that not only made the site difficult to edit, but also made the end result look a little messy.

The solution was to move some of the existing layouts to a single type and remove those layouts from the selection. This made it easier to edit pages and also easier to predict how the site would look when we made some style changes.

Whilst it is certainly possible to do this by hand, it's not easy to track down every instance of a particular layout and convert them all. I also wanted a more automatic approach to the solution so that I could run a drush command and convert all of one type of Layout Paragraph to another.

In this article we will look at the structure of the Layout Paragraphs module and when how to move a Layout Paragraph from one layout to another using PHP.

Layout Paragraphs Structure

Before we look at how to move layout pargraphs we should take a quick look at the structure of a Layout Paragraph. The structure of the Layout Paragraphs system is deceptivly simple for what appears to be a nested Paragraph structure.

Let's say we had a Layout Paragraph with the layout of "layout_twocol" that contains two other Paragraphs. This would be created like any other Paragraph using the Drupal administration pages.

Every Paragraph in the site has a field in the paragraphs_item_field_data table called behavior_settings where information about the Paragraph can be stored. This is a PHP serialised array field that is used by different modules to provide additional settings to the Paragraph to influence its functionality and create custom features. The Layout Paragraphs module uses this feature to store data pertaining to the layout.

For the purposes of our layout the Layout Paragraph has the following structure in the behavior_settings field.

array (
  'layout_paragraphs' => array (
    'layout' => 'layout_twocol',
    'config' => array (
      'label' => '',
    ),
    'parent_uuid' => NULL,
    'region' => NULL
  ),
)

In addition to the Paragraph type being "layout", this tells us what sort of layout is in use, which in this case is "layout_twocol". We can grab hold of this information within the site by using a couple of methods on the Paragraph entity itself.

The behavior settings sometimes has an additional setting of more_items, which is used internally by the Layout Paragraphs module when a layout is manually moved from one type to another. This isn't needed outside of this manual process. 

The getBehaviorSetting() method takes two parameters, the first being the plugin ID to find data for (which is layout_paragraphs for this purpose) and the data that we would like to get access to.

$paragraphLayoutType = $paragraph->getBehaviorSetting('layout_paragraphs', 'layout');

Since the layout is a string the return here contains a single value of the layout in use.

Alternatively, we can ask for all of the behavior settings in one go, and then extract the information we need from the relevant part of the array.

$parentBehaviorSettings = $paragraph->getAllBehaviorSettings();
$paragraphLayoutType = $parentBehaviorSettings['layout_paragraphs']['layout'];

Now that we have the layout parent we need to find the children that belong to the parent layout.

The Paragraphs that belong in this layout sit at the same level as the Paragraph in the database. Whilst I have called them children, there is actually no nesting or other recusion going on here. Consider the parent/child relationship more of an aid to understanding.

Each child Paragraph that belongs to this layout will get a behavior_settings that will point to the parent Layout Paragraph (via a uuid). This setting will also contain the region within that Layout Paragraph that the child Paragraph belongs to.

Here's an example of a child of a Layout Paragraph.

array (
  'layout_paragraphs' => array (
    'parent_uuid' => 'd635a666-f36f-4d76-a8a5-deee32736440',
    'region' => 'first',
  ),
)

The child Paragraphs can be found by loading all the Paragrpahs within a field and looping through them to build up the list of layouts. This is done internally by the module when loading and rendering the Paragraphs.

You can have a look at this structure if you like, using the following database query.

select nfp.bundle, nfp.entity_id, pi.uuid, pifd.`type`, pifd.behavior_settings
from node__field_paragraphs as nfp
inner join paragraphs_item pi on pi.id = nfp.field_paragraphs_target_id and pi.revision_id = nfp.field_paragraphs_target_revision_id 
inner join paragraphs_item_field_data as pifd on pifd.id = nfp.field_paragraphs_target_id and pifd.revision_id = nfp.field_paragraphs_target_revision_id 
where nfp.entity_id = 19
order by nfp.delta asc;

This looks at the structure of Paragraphs in node 19, in the field field_paragraphs. You may have to alter the above query to get it working with your setup.

Moving Layout Paragraphs

Using the above information about the structure of the layout Paragraphs we can now move them around.

First, we need to deine a couple of variables that can be used to migrate our layout Paragraph from one layout to another.

$matchLayoutParagraphType = 'layout_twocol_right';

$newLayout = 'layout_onecol';

$layoutMovementMap = [
  'mappings' => [
    'first' => 'content',
    'second' => 'content',
  ],
];

The variables we have defined here are:

  • $matchLayoutParagraphType - This is the layout that we want to change.
  • $newLayout - This is the layout that we want to change that layout to.
  • $layoutMovementMap - This contains an array that is used to map layout regions from one region to another.

Using these variables we can now find the Layout Paragraphs that have the layout in question. As there isn't a way to query PHP serialised arrays we need to instead load all of the Paragraphs in the system that have the type "layout".

$paragraphStorage = $this->entityTypeManager->getStorage('paragraph');

// Load all layout paragraphs.
$pids = $paragraphStorage->getQuery()
  ->condition('type', 'layout')
  ->accessCheck(FALSE)
  ->execute();

We can then loop through all of the layout Paragraphs to find the ones with the particular layout that we want by extracting the information from the behavior settings.

foreach ($pids as $pid) {
  // Load the layout paragraph.
  /** @var \Drupal\paragraphs\ParagraphInterface $paragraph */
  $paragraph = $paragraphStorage->load($pid);
  
  // -- Move the layout here.
}

For each Paragraph we need to use the getBehaviorSetting() method to find the layout type we are looking for.

// Extract the layout behaviors setting for the layout paragraph and
// match the name of the layout.
$paragraphLayoutType = $paragraph->getBehaviorSetting('layout_paragraphs', 'layout');

if ($paragraphLayoutType === $matchLayoutParagraphType) {
  // Move layout paragraph code.
}

Once we have varified that the layout type is correct we can then load the entire behavior settings array for the Paragraph and update the layout in the layout_paragraphs section before saving.

// Save the current Paragraph with the new setting.
$parentBehaviorSettings = $paragraph->getAllBehaviorSettings();
$parentBehaviorSettings['layout_paragraphs']['layout'] = $newLayout;
$paragraph->setBehaviorSettings('layout_paragraphs', $parentBehaviorSettings['layout_paragraphs']);
$paragraph->save();

That changes the root Paragraph, but we still need to repoint the children to the right regions inside the layout.

This process is slightly more complex, but involves loading all of the Paragraphs in the field and looping through them until we find one that is connected to the parent Paragraph (using the uuid). Once we have one of the child Paragraphs we then need to re-map the region it has to the new region from the $layoutMovementMap variable.

// Grab all other Paragraphs from the same field.
$field_name = $paragraph->get('parent_field_name')->value;

/** @var \Drupal\paragraphs\ParagraphInterface $paragraph */
$contentParagraphs = $parentEntity->get($field_name)->referencedEntities();

foreach ($contentParagraphs as $contentParagraph) {
  // Loop through the Paragraphs to find items that call this Paragraph
  // the parent.
  $behaviorSettings = $contentParagraph->getAllBehaviorSettings();
  if (isset($behaviorSettings['layout_paragraphs']['parent_uuid']) && $behaviorSettings['layout_paragraphs']['parent_uuid'] === $paragraph->uuid()) {
    // If we have a match then repoint the region and save the
    // Paragraph.
    $behaviorSettings['layout_paragraphs']['region'] = $layoutMovementMap[$behaviorSettings['layout_paragraphs']['region']];
    $contentParagraph->setBehaviorSettings('layout_paragraphs', $behaviorSettings['layout_paragraphs']);
    $contentParagraph->save();
  }
}

After running this code all the Layout Paragraphs that have a layout of layout_twocol_right are now converted to the layout layout_onecol, with the content placed into the correct parts.

This code can (certainly) be improved by wrapping it in a batch system. The site I was working on at the time was quite small and only had around 100 Paragraphs to update, so the batch system wasn't used.

Conclusion

Moving a layout Paragraph from one type to another means altering both the parent and chlid Paragraphs. I was able to create a simple service that changed all of the Layout Paragraphs on a site with around 200 pages in a few seconds. The layout mapping was pretty much hard coded, but it suited my purposes quite well.

The hardest part in all of this was finding out the mechanism involved in performing these actions. Although the Layout Paragraphs module does have a mechanism to alter the layout of a Layout Paragraph in the administration area, it doesn't perform this move in one go. In that case the change is made as the node is saved using the move_items part of the behavior settings system.

In the end, the layouts that weren't needed on the site could then be uninstalled and removed, which simplified the backend editor experience. The site went from nearly 12 different layouts to just 3, with all of the existing layouts mapped correctly.

If I get enough interest in this I'll consider making this into a module and contributing it. Or even attempt contributing it back to the Layout Paragraphs module. My only concern is how to create the user interface since creating the mapping system might be quite complex. Making this hard coded in the system was pretty simple.

Add new comment

The content of this field is kept private and will not be shown publicly.