“Oh snap”, said the project manager. “The client has this whole range of rich articles they probably are expecting to still work after the migration!”
The project was a relaunch of a Drupal / Commerce 1 site, redone for Drupal 8 and Commerce 2. A couple of weeks before the relaunch, and literally days before the client was allowed in to see the staging site, we found out we had forgotten a whole range of rich articles where the client had carefully crafted landing pages, campaign pages and “inspiration” pages (this is a interior type of store). The pages were panel nodes, and it had a handful of different panel panes (all custom).
In the new site we had made Layout builder available to make such pages.
We had 2 options:
- Redo all of them manually with copy paste.
- Migrate panel nodes into layout builder enabled nodes.
“Is that even possible?”, said the project manager.
Well, we just have to try, won’t we?
Creating the destination node type
First off, I went ahead and created a new node type called “inspiration page”. And then I enabled layout builder for individual entities for this node type.
Now I was able to create “inspiration page” landing pages. Great!
Creating the migration
Next, I went ahead and wrote a migration plugin for the panel nodes. It ended up looking like this:
id: mymodule_inspiration
label: mymodule inspiration
migration_group: mymodule_migrate
migration_tags:
- mymodule
source:
# This is the source plugin, that we will create.
plugin: mymodule_inspiration
track_changes: TRUE
# This is the key in the database array.
key: d7
# This means something to the d7_node plugin, that we inherit from.
node_type: panel
# This is used to create a path (not covered in this article).
constants:
slash: '/'
process:
type:
plugin: default_value
# This is the destination node type
default_value: inspiration_page
# Copy over some values
title: title
changed: changed
created: created
# This is the important part!
layout_builder__layout: layout
path:
plugin: concat
source:
- constants/slash
- path
destination:
plugin: entity:node
# This is the destination node type
default_bundle: inspiration_page
dependencies:
enforced:
module:
- mymodule_migrate
As mentioned in the annotated configuration, we need a custom source plugin for this. So, let’s take a look at how we make that:
Creating the migration plugin
If you have a module called “mymodule”, you create a folder structure like so, inside it (just like other plugins):
src/Plugin/migrate/source
And let’s go ahead and create the “Inspiration” plugin, a file called Inspiration.php:
<?php
namespace Drupal\mymodule_migrate\Plugin\migrate\source;
use Drupal\Component\Uuid\UuidInterface;
use Drupal\Core\Entity\EntityManagerInterface;
use Drupal\Core\Extension\ModuleHandlerInterface;
use Drupal\Core\State\StateInterface;
use Drupal\layout_builder\Section;
use Drupal\layout_builder\SectionComponent;
use Drupal\migrate\Plugin\MigrationInterface;
use Drupal\migrate\Row;
use Drupal\node\Plugin\migrate\source\d7\Node;
use Symfony\Component\DependencyInjection\ContainerInterface;
/**
* Panel node source, based on panes inside a panel page.
*
* @MigrateSource(
* id = "mymodule_inspiration"
* )
*/
class Inspiration extends Node {
/**
* Uuid generator.
*
* @var \Drupal\Component\Uuid\UuidInterface
*/
protected $uuid;
/**
* Inspiration constructor.
*/
public function __construct(
array $configuration,
$plugin_id,
$plugin_definition,
MigrationInterface $migration,
StateInterface $state,
EntityManagerInterface $entity_manager,
ModuleHandlerInterface $module_handler,
UuidInterface $uuid
) {
parent::__construct($configuration, $plugin_id, $plugin_definition,
$migration, $state, $entity_manager, $module_handler);
$this->uuid = $uuid;
}
/**
* {@inheritdoc}
*/
public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition, MigrationInterface $migration = NULL) {
return new static(
$configuration,
$plugin_id,
$plugin_definition,
$migration,
$container->get('state'),
$container->get('entity.manager'),
$container->get('module_handler'),
$container->get('uuid')
);
}
}
Ok, so this is the setup for the plugin. For this specific migration, there were some weird conditions for which of the panel nodes were actually inspiration pages. If I copy-pasted it here, you would think I was insane, but for now I can just mention that we were overriding the public function query. You may or may not need to do the same.
So, after getting the query right, we are going to do some work inside of the prepareRow function:
/**
* {@inheritdoc}
*/
public function prepareRow(Row $row) {
$result = parent::prepareRow($row);
if (!$result) {
return $result;
}
// Get all the panes for this nid.
$did = $this->select('panels_node', 'pn')
->fields('pn', ['did'])
->condition('pn.nid', $row->getSourceProperty('nid'))
->execute()
->fetchField();
// Find all the panel panes.
$panes = $this->getPanelPanes($did);
$sections = [];
$section = new Section('layout_onecol');
$sections[] = $section;
foreach ($panes as $delta => $pane) {
if (!$components = $this->getComponents($pane)) {
// You must decide what you want to do when a panel pane can not be
// converted.
continue;
}
// Here we used to have some code dealing with changing section if this
// and that. You may or may not need this.
foreach ($components as $component) {
$section->appendComponent($component);
}
}
$row->setSourceProperty('layout', $sections);
// Don't forget to migrate the "path" part. This is left out for this
// article.
return $result;
}
Now you may notice there are some helper methods there. They look something like this:
/**
* Helper.
*/
protected function getPanelPanes($did) {
$q = $this->select('panels_pane', 'pp');
$q->fields('pp');
$q->condition('pp.did', $did);
$q->orderBy('pp.position');
return $q->execute();
}
/**
* Helper to get components back, based on pane configuration.
*/
protected function getComponents($pane) {
$configuration = @unserialize($pane["configuration"]);
if (empty($configuration)) {
return FALSE;
}
$region = 'content';
// Here would be the different conversions between panel panes and blocks.
// This would be very varying based on the panes, but here is one simple
// example:
switch ($pane['type']) {
case 'custom':
// This is the block plugin id.
$plugin_id = 'my_custom_content_block';
$component = new SectionComponent($this->uuid->generate(), $region, [
'id' => $plugin_id,
// This is the title of the block.
'title' => $configuration['title'],
// The following are configuration options for this block.
'image' => '',
'text' => [
// These values come from the configuration of the panel pane.
'value' => $configuration["body"],
'format' => 'full_html',
],
'url' => $configuration["url"],
]);
return [$component];
default:
return FALSE;
}
}
So there you have it! Since we now have amazing tools in Drupal 8 (namely Layout builder and Migrate) there is not task that deserves the question “Is that even possible?”.
To finish off, let's have an animated gif called "inspiration". And I hope this will give some inspiration to other people migrating landing pages into layout builder.
Douglas Gough•Thursday, Mar 7th 2019 (over 5 years ago)
Thanks for sharing this. Real world examples of custom migrations are hard to find, so I'm sure this will be helpful to a lot of people.
eiriksm•Thursday, Mar 7th 2019 (over 5 years ago)
Thanks, glad to hear it. Thanks for commenting.
ryanlebreton-gmu•Tuesday, May 19th 2020 (over 4 years ago)
Thanks in advance for any guidance you can provide!
`
//Add an inline block of 'basic' type to a section
//'basic' blocks only have a field called "body" of type "Text (formatted, long, with summary)"
$section = new Section('two_column');
$testcomponent = new SectionComponent($this->uuid->generate(), 'first', [
'id' => 'inline_block:basic',
'label' => 'Test component label',
'label_display' => 'visible',
//This doesn't work
'body',[
'value'=>'Lorem ipsum dolor sit amet.',
'format'=>'limited_html_text',
]
]);
$section->appendComponent($testcomponent);
`
eiriksm•Wednesday, May 20th 2020 (over 4 years ago)
Inline blocks are a completely different story. They are (behind the scenes) actual content entities, and they are only referenced in their respective plugin definitions.
Personally I do not use them, but if I were to adapt your example, I think you are looking for something like this:
```
use Drupal\block_content\Entity\BlockContent;
use Drupal\layout_builder\Section;
use Drupal\layout_builder\SectionComponent;
$section = new Section('layout_onecol');
// Ideally you would inject an entity type manager in the plugin and use this
// instead:
// $block = $this->entityTypeManager->getStorage('block_content')->create()
$block = BlockContent::create([
'type' => 'basic',
]);
$block->set('body', [
'value' => 'Lorem ipsum dolor sit amet.',
'format' => 'limited_html_text',
]);
$block->save();
$testcomponent = new SectionComponent($this->uuid->generate(), 'content', [
'id' => 'inline_block:basic',
'label' => 'Test component label',
'label_display' => 'visible',
'block_revision_id' => $block->getRevisionId(),
]);
$section->appendComponent($testcomponent);
```
Hope that either helps you, or someone else looking for something similar 🥂
ryanlebreton-gmu•Wednesday, May 20th 2020 (over 4 years ago)
This is exactly what I was looking for. I was starting to question whether inline blocks needed to be handled differently and was also wondering how revisions fit into the puzzle, and you answered both of those. Thanks again, I really appreciate your time.
eiriksm•Wednesday, May 20th 2020 (over 4 years ago)
ryanlebreton-gmu•Thursday, May 21st 2020 (over 4 years ago)
erik-seifert•Thursday, Sep 10th 2020 (about 4 years ago)
$node = Node::create([
'type' => 'baumeister_page',
'langcode' => 'en',
'uid' => 1,
'title' => 'Welcome to baumeister',
'status' => 1,
'path' => [
'alias' => '/home',
]
]);
$section = new Section('one_column');
$component = new SectionComponent(\Drupal::service('uuid')->generate(), 'main', [
'id' => 'inline_block:cmp_text',
'reusable' => FALSE,
'block_serialized' => serialize(BlockContent::create([
'type' => 'cmp_text',
'field_cmp_text_content' => 'Yo'
]))
]);
$section->appendComponent($component);
$node->layout_builder__layout->setValue([$section]);
$node->save();
```
darko-hrgovic•Friday, Jun 18th 2021 (over 3 years ago)
Just a note that in the OP's code, the body isn't an array element. Maybe it's a typo? Would it work if corrected, I wonder?
` 'body',[
'value'=>'Lorem ipsum dolor sit amet.',
'format'=>'limited_html_text',
]`
`
'body' => [
'value'=>'Lorem ipsum dolor sit amet.',
'format'=>'limited_html_text',
]`
eiriksm•Friday, Jun 18th 2021 (over 3 years ago)
Do you want to comment?
This article uses github for commenting. To comment, you can visit https://github.com/eiriksm/eiriksm.dev-comments/issues/7.