Migration Deep-dive
Panelizer to Layout Builder
Brian Tofte-Schumacher
Senior Developer at Forum One
I live in Seattle. In addition to loving Drupal, I enjoy cooking, photography, and getting out in nature.
Contact Information
Pronouns: he/him/his
Twitter: @briantschu
Email: btofte-schumacher@forumone.com
Case Study: EPA.gov
Layout Builder
Migration Lifecycle
Implementation: Panelizer to Layout Builder
Resources and Tips
Q&A
Today’s Agenda
Partnering with EPA to migrate epa.gov from Drupal 7 to Drupal 8.
D7 used Panelizer with Panes on 1000s of pages. Migrating into Layout Builder in D8.
Need custom migration plugins to gather data from Panelizer and transform it for Layout Builder.
Layout Builder considerations
Layout Builder considerations
Layout Builder considerations
Case Study: EPA.gov
Layout Builder
Migration Lifecycle
Implementation: Panelizer to Layout Builder
Resources and Tips
Q&A
Today’s Agenda
Case Study: EPA.gov
Layout Builder
Migration Lifecycle
Implementation: Panelizer to Layout Builder
Resources and Tips
Q&A
Today’s Agenda
Extract, transform, load.
Visual representation of ETL
Source: drupal.org
Simplified migration yaml
id: upgrade_d7_node_page
migration_group: migrate_drupal_7
label: 'Nodes (Basic page)'
source:
plugin: epa_node
node_type: page
process:
nid:
-
plugin: get
source: tnid
destination:
plugin: 'entity:node'
default_bundle: page
Case Study: EPA.gov
Layout Builder
Migration Lifecycle
Implementation: Panelizer to Layout Builder
Resources and Tips
Q&A
Today’s Agenda
What we’re working toward
id: upgrade_d7_node_page_panelizer
migration_group: migrate_drupal_7
label: 'Panelizer Nodes (Basic page)'
source:
plugin: epa_panelizer_node
node_type: page
process:
layout_builder__layout:
plugin: epa_panes_to_lb_section
source: panes
destination:
plugin: 'entity:node'
default_bundle: page
Our game plan.
Understand the database structures
Get to know Panelizer and Layout Builder tables
1
Query the database
Write a source plugin
2
3
Transform the panes into LB Blocks
Write a process plugin
4
Implement the migration configuration
Use the source and process plugin
Panelizer database tables
panelizer_entity
entity_type
entity_id
revision_id
did
panels_display
did
layout
panels_pane
pid
did
panel
type
subtype
shown
position
configuration
style
Panelizer database tables
did | layout |
500 | rd_homepage |
panels_display
entity_type | entity_id | revision_id | did |
node | 1 | 2 | 500 |
panelizer_entity
Panelizer database tables
pid | did | panel | type | subtype | shown | position | configuration |
101 | 500 | main_col | fieldable_panels_pane | vid:122087 | 1 | 0 | a:1:{s:9:"view_mode";s:4:"full |
102 | 500 | rda1 | node_content | node_content | 1 | 0 | a:10:{s:5:"links";i:0;s:4:"page" |
103 | 500 | rda2 | fieldable_panels_pane | vid:180791 | 1 | 0 | a:1:{s:9:"view_mode";s:4:"full |
104 | 500 | rdb1 | fieldable_panels_pane | vid:130473 | 1 | 0 | a:1:{s:9:"view_mode";s:4:"full |
105 | 500 | rdb1 | fieldable_panels_pane | vid:131871 | 1 | 1 | a:1:{s:9:"view_mode";s:4:"full |
106 | 500 | rdb2 | fieldable_panels_pane | vid:130471 | 1 | 1 | a:1:{s:9:"view_mode";s:4:"full |
107 | 500 | rdb2 | fieldable_panels_pane | vid:168115 | 1 | 1 | a:1:{s:9:"view_mode";s:4:"full |
108 | 500 | rdc1 | fieldable_panels_pane | vid:131885 | 1 | 1 | a:1:{s:9:"view_mode";s:4:"full |
109 | 500 | rdc2 | fieldable_panels_pane | vid:135667 | 1 | 1 | a:1:{s:9:"view_mode";s:4:"full |
110 | 500 | bottom | views_panes | news_release_list | 1 | 1 | a:8:{s:9:"more_link";i:1;s:14:"i |
111 | 500 | sidebar | fieldable_panels_pane | vid:159581 | 1 | 0 | a:1:{s:9:"view_mode";s:4:"full |
112 | 500 | sidebar | fieldable_panels_pane | vid:175491 | 1 | 1 | a:1:{s:9:"view_mode";s:4:"full |
panels_pane
Fieldable Panels Panes
field_data_field_epa_fpp_body
Layout builder database table
node_revision__layout_builder__layout
bundle | deleted | entity_id | revision_id | langcode | delta | layout_builder__layout_section |
page | 0 | 1 | 2 | en | 0 | [serialized data] |
Let’s write a source plugin!
Source plugin skeleton
[epa_migrations/src/Plugin/migrate/source/EpaPanelizerNode.php]
/**
* Load Nodes that will be migrated into Layout Builder.
*
* @MigrateSource(
* id = "epa_panelizer_node",
* )
*/
class EpaPanelizerNode extends Node {
public function query() { … }
public function prepareRow() { … }
}
Limit the Node query
[epa_migrations/src/Plugin/migrate/source/EpaPanelizerNode.php]
public function query() {
// Get the default Node query.
$query = parent::query();
// Limit the query to nodes that have Panelizer data.
$query->innerJoin('panelizer_entity', 'pe', 'n.vid = pe.revision_id');
$query->innerJoin('panels_display', 'pd', 'pe.did = pd.did');
$query->condition('pe.did', 0, '<>');
$query->condition('pd.layout', 'onecol_page', '<>');
return $query;
}
Get the display id
[epa_migrations/src/Plugin/migrate/source/EpaPanelizerNode.php]
public function prepareRow(Row $row) {
// Always include this fragment at the beginning of every prepareRow()
// implementation, so parent classes can ignore rows.
if (parent::prepareRow($row) === FALSE) {
return FALSE;
}
// Get the Display ID for the node in its current revision.
$did = $this->select('panelizer_entity', 'pe')
->fields('pe', ['did'])
->condition('pe.revision_id', $row->getSourceProperty('vid'))
->execute()->fetchField();
…
Get the layout for this display
[epa_migrations/src/Plugin/migrate/source/EpaPanelizerNode.php]
public function prepareRow(Row $row) {
…
// Get the Panelizer layout for this display.
$layout = $this->select('panels_display', 'pd')
->fields('pd', ['layout'])
->condition('pd.did', $did)
->execute()->fetchField();
// Set the 'layout' property.
$row->setSourceProperty('layout', $layout);
…
Get the panes for this display
[epa_migrations/src/Plugin/migrate/source/EpaPanelizerNode.php]
public function prepareRow(Row $row) {
…
// Fetch the panes and add the result as a source property.
$panes = $this->select('panels_pane', 'pp')
->fields('pp')
->condition('pp.did', $did)
->orderBy('pp.position')->orderBy('pp.panel')
->execute()->fetchAll();
$row->setSourceProperty('panes', $panes);
parent::prepareRow($row);
}
Source plugin in use!
id: upgrade_d7_node_page_panelizer
migration_group: migrate_drupal_7
label: 'Panelizer Nodes (Basic page)'
source:
plugin: epa_panelizer_node
node_type: page
process:
layout_builder__layout:
plugin: epa_panes_to_lb_section
source: panes
destination:
plugin: 'entity:node'
default_bundle: page
Now, let’s process these Nodes!
Refine the layout builder process
source:
plugin: epa_panelizer_node
node_type: page
process:
layout_builder__layout:
-
plugin: single_value
source: panes
-
plugin: epa_panes_to_lb_section
destination:
plugin: 'entity:node'
default_bundle: page
Process plugin skeleton
[epa_migrations/src/Plugin/migrate/process/EpaPanesToLbSection.php]
use Drupal\layout_builder\Section;
use Drupal\layout_builder\SectionComponent;
/**
* Given a set of panes, returns a layout builder section.
*
* @MigrateProcessPlugin(
* id = "epa_panes_to_lb_section"
* )
*/
class EpaPanelizerNode extends Node {
public function transform() { … }
}
Create a Section
[epa_migrations/src/Plugin/migrate/process/EpaPanesToLbSection.php]
public function transform($value, MigrateExecutableInterface
$migrate_executable, Row $row, $destination_property) {
// Hard-coding logic for our rd_homepage layout.
$sections[0] = new Section(‘rd_homepage);
// In reality, this code will pull the layout from the Row to
// determine how many sections to create and which layout to use
// and then we’d iterate over the Panes stored in $value to pull
// out the actual Pane data.
…
Create HTML paragraph
[epa_migrations/src/Plugin/migrate/process/EpaPanesToLbSection.php]
public function transform($value, MigrateExecutableInterface
$migrate_executable, Row $row, $destination_property) {
…
$body_field = $this->select('field_data_field_epa_fpp_body', 'fpp_body')
->condition(‘fpp_body.vid’, '122087');
$body_field->addField('fpp_body', 'field_epa_fpp_body_value', 'value');
$body_field->execute()->fetchAssoc();
// Create an HTML paragraph with WYSIWYG content.
$html_paragraph = Paragraph::create(['type' => 'html']);
$html_paragraph->set('field_body', $body_field);
$html_paragraph->isNew();
$html_paragraph->save();
…
Create Box paragraph
[epa_migrations/src/Plugin/migrate/process/EpaPanesToLbSection.php]
public function transform($value, MigrateExecutableInterface
$migrate_executable, Row $row, $destination_property) {
…
// Create Box paragraph container.
$box_paragraph = Paragraph::create(['type' => 'box']);
$box_paragraph->set('field_paragraphs', $html_paragraph);
$box_paragraph->isNew();
$box_paragraph->save();
…
Create Block
[epa_migrations/src/Plugin/migrate/process/EpaPanesToLbSection.php]
public function transform($value, MigrateExecutableInterface
$migrate_executable, Row $row, $destination_property) {
…
// Build a main_col Block entity.
$block = $this->entityTypeManager->getStorage('block_content')
->create([
'info' => 'Paragraph Block',
'type' => 'paragraph',
'reusable' => 0,
'field_paragraphs' => $box_paragraph,
]
);
…
Wrap Block in Section Component
[epa_migrations/src/Plugin/migrate/process/EpaPanesToLbSection.php]
public function transform($value, MigrateExecutableInterface
$migrate_executable, Row $row, $destination_property) {
…
$region = 'main_col';
$component = new SectionComponent($this->uuid->generate(), $region,
[
'id' => 'inline_block:paragraph',
'label' => 'Paragraph Block',
'label_display' => 'false',
'block_serialized' => serialize($block),
'context_mapping' => [],
]);
…
Add Section component to Section
[epa_migrations/src/Plugin/migrate/process/EpaPanesToLbSection.php]
public function transform($value, MigrateExecutableInterface
$migrate_executable, Row $row, $destination_property) {
…
$sections[0]->appendComponent($component);
// Build the rest of the Section Components for the other regions.
…
return $sections;
// Section
// Section Component
// Block → Paragraph
}
Process plugin in use!
source:
plugin: epa_panelizer_node
node_type: page
process:
layout_builder__layout:
-
plugin: single_value
source: panes
-
plugin: epa_panes_to_lb_section
destination:
plugin: 'entity:node'
default_bundle: page
Test the migration
Limit the migration to one node id
[epa_migrations/src/Plugin/migrate/source/EpaPanelizerNode.php]
public function query() {
// Get the default Node query.
$query = parent::query();
$query->innerJoin('panelizer_entity', 'pe', 'n.vid = pe.revision_id');
$query->innerJoin('panels_display', 'pd', 'pe.did = pd.did');
$query->condition('pe.did', 0, '<>');
$query->condition('pd.layout', 'onecol_page', '<>');
$query->condition('n.nid', 37421);
return $query;
}
Drumroll, please….
Layout Builder considerations
Case Study: EPA.gov
Layout Builder
Migration Lifecycle
Implementation: Panelizer to Layout Builder
Resources and Tips
Q&A
Today’s Agenda
Generating upgrade configs
--legacy-db-url=mysql://user:pass@12.34.56.78/d7db
--legacy-root=http://myd7site.com
--configure-only
Migration group
id: migrate_drupal_7
label: 'Import from Drupal 7'
description: 'Generated from drush migrate-upgrade --configure-only'
source_type: 'Drupal 7'
shared_configuration:
source:
key: drupal7
database:
driver: mysql
username: 'username'
password: 'password'
host: 'localhost'
port: '3306'
database: 'database'
prefix: null
Additional Tips
Migration Planning
Layout Builder
Migration Lifecycle
Implementation: Panelizer to Layout Builder
Migration Validation
Q&A
Today’s Agenda
Questions?
Feedback?
Thank you!
Brian Tofte-Schumacher, Senior Developer
btofte-schumacher@forumone.com
@briantschu