1 of 51

Migration Deep-dive

Panelizer to Layout Builder

2 of 51

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

3 of 51

Case Study: EPA.gov

Layout Builder

Migration Lifecycle

Implementation: Panelizer to Layout Builder

Resources and Tips

Q&A

Today’s Agenda

4 of 51

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.

5 of 51

Layout Builder considerations

  • What kind of custom layouts do content editors want to create?
  • Can all content types (and creators) use these layouts?
  • Will all content of a specific type need layout builder?

6 of 51

Layout Builder considerations

  • What kind of custom layouts do content editors want to create?
  • Can all content types (and creators) use these layouts?
  • Will all content of a specific type need layout builder?

7 of 51

Layout Builder considerations

  • What kind of custom layouts do content editors want to create?
  • Can all content types (and creators) use these layouts?
  • Will all content of a specific type need layout builder?

8 of 51

Case Study: EPA.gov

Layout Builder

Migration Lifecycle

Implementation: Panelizer to Layout Builder

Resources and Tips

Q&A

Today’s Agenda

9 of 51

10 of 51

Case Study: EPA.gov

Layout Builder

Migration Lifecycle

Implementation: Panelizer to Layout Builder

Resources and Tips

Q&A

Today’s Agenda

11 of 51

Extract, transform, load.

  • Extract → Source
  • Transform → Process
  • Load → Destination

12 of 51

Visual representation of ETL

Source: drupal.org

13 of 51

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

14 of 51

Case Study: EPA.gov

Layout Builder

Migration Lifecycle

Implementation: Panelizer to Layout Builder

Resources and Tips

Q&A

Today’s Agenda

15 of 51

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

16 of 51

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

17 of 51

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

18 of 51

Panelizer database tables

did

layout

500

rd_homepage

panels_display

entity_type

entity_id

revision_id

did

node

1

2

500

panelizer_entity

19 of 51

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

20 of 51

Fieldable Panels Panes

  • Additional tables for the fields on the panes
  • Only concerned about the body field for this example

field_data_field_epa_fpp_body

21 of 51

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]

22 of 51

Let’s write a source plugin!

23 of 51

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() { … }

}

24 of 51

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;

}

25 of 51

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();

26 of 51

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);

27 of 51

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);

}

28 of 51

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

29 of 51

Now, let’s process these Nodes!

30 of 51

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

31 of 51

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() { … }

}

32 of 51

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.

33 of 51

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();

34 of 51

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();

35 of 51

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,

]

);

36 of 51

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' => [],

]);

37 of 51

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

}

38 of 51

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

39 of 51

Test the migration

  • drush migrate-import upgrade_d7_node_page_panelizer
  • Add a node id condition to the source plugin query
  • Only 1 node will be migrated

40 of 51

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;

}

41 of 51

Drumroll, please….

42 of 51

43 of 51

44 of 51

Layout Builder considerations

  • What kind of custom layouts do content editors want to create?
  • Can all content types (and creators) use these layouts?
  • Will all content of a specific type need layout builder?

45 of 51

Case Study: EPA.gov

Layout Builder

Migration Lifecycle

Implementation: Panelizer to Layout Builder

Resources and Tips

Q&A

Today’s Agenda

46 of 51

Generating upgrade configs

  • Install the migrate_upgrade module
  • Run drush migrate-upgrade

--legacy-db-url=mysql://user:pass@12.34.56.78/d7db

--legacy-root=http://myd7site.com

--configure-only

  • Use the generated configs as a baseline

47 of 51

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

48 of 51

Additional Tips

  • Use drush generate to create Plugin scaffold files
  • Create Content Models and documentation before coding
  • Use Layout Builder Restrictions module to clean up the layout builder interface

49 of 51

Migration Planning

Layout Builder

Migration Lifecycle

Implementation: Panelizer to Layout Builder

Migration Validation

Q&A

Today’s Agenda

50 of 51

Questions?

Feedback?

51 of 51

Thank you!

Brian Tofte-Schumacher, Senior Developer

btofte-schumacher@forumone.com

@briantschu