Using Migrate as a replacement for Update Hooks

As a developer I tend to be lazy. I'm always searching for tools or shortcuts to make my live more comfortable.

Recently I stumbled across a giving an example on how to use migrate instead of a custom update script to update existing entities. Having no use for it at this time I saved it in my "maybe useful later"-part of my brain ... and forgot about it.

However, last week I had to add an existing field to the form display of all Paragraphs types in a project. Additionally a new field should be added and configured. Because I didn't want to configure the fields manually (remember? I'm a lazy developer!) for all Paragraphs types (there are about 80 of them in this project) I normally would have written an update hook for this task. But then I remembered the tweet and thought "wouldn't it be also possible to update the configuration using migrate?".

What do we need?

The first part of the task is easy: to display an existing field in the form of an entity you simply drag it from the "hidden" section to the content section.

Paragraphs form display

After moving the field "Published" into the content section, I exported the configuration changes to see what happened and got the following result for core.entity_form_display.paragraph.text.default.yml:

Configuration changes in Paragraphs form display

So in my migration I have to replicate exactly this configuration change for all Paragraphs types.

Migrating form display settings

In the migration I need a source plugin for all available Paragraphs types first. Because I already made the necessary changes to the form display of Paragraphs type "Text" the source plugin also needs the possibility to exclude certain items (well, I eventually could have reverted the previous configuration changes and start over with a recent database backup, but ...).

  1. <?php
  2.  
  3. namespace Drupal\up_migrate\Plugin\migrate\source;
  4.  
  5. use ArrayObject;
  6. use Drupal\Core\StringTranslation\StringTranslationTrait;
  7. use Drupal\migrate\Plugin\migrate\source\SourcePluginBase;
  8. use Drupal\migrate\Plugin\MigrationInterface;
  9. use Drupal\paragraphs\Entity\ParagraphsType;
  10.  
  11. /**
  12.  * Source plugin for ParagraphsType.
  13.  *
  14.  * @MigrateSource(
  15.  *   id = "up_paragraphs_type",
  16.  *   source_module = "up_migrate"
  17.  * )
  18.  */
  19. class UpParagraphsType extends SourcePluginBase {
  20.  
  21.   use StringTranslationTrait {
  22.     t as t_original;
  23.   }
  24.  
  25.   /**
  26.    * List of paragraphs types to exclude.
  27.    *
  28.    * @var array
  29.    */
  30.   protected $exclude = [];
  31.  
  32.   /**
  33.    * List of paragraph types.
  34.    *
  35.    * @var array
  36.    */
  37.   protected $items = [];
  38.  
  39.   /**
  40.    * {@inheritdoc}
  41.    */
  42.   protected function t($string, array $args = [], array $options = []) {
  43.     if (empty($options['context'])) {
  44.       $options['context'] = 'up_migrate';
  45.     }
  46.  
  47.     return $this->t_original($string, $args, $options);
  48.   }
  49.  
  50.   /**
  51.    * {@inheritdoc}
  52.    */
  53.   public function __construct(array $configuration, $plugin_id, $plugin_definition, MigrationInterface $migration) {
  54.     parent::__construct($configuration, $plugin_id, $plugin_definition, $migration);
  55.  
  56.     if (isset($configuration['exclude'])) {
  57.       $this->exclude = $configuration['exclude'];
  58.     }
  59.   }
  60.  
  61.   /**
  62.    * {@inheritdoc}
  63.    */
  64.   public function fields() {
  65.     return [
  66.       'id' => $this->t('ID'),
  67.       'label' => $this->t('Label'),
  68.     ];
  69.   }
  70.  
  71.   /**
  72.    * {@inheritdoc}
  73.    */
  74.   public function getIds() {
  75.     $ids['id']['type'] = 'string';
  76.     return $ids;
  77.   }
  78.  
  79.   /**
  80.    * Return a comma-separated list of paragraph type ids.
  81.    */
  82.   public function __toString() {
  83.     return implode(', ', array_column($this->items, 'id'));
  84.   }
  85.  
  86.   /**
  87.    * {@inheritdoc}
  88.    */
  89.   protected function initializeIterator() {
  90.     $this->items = [];
  91.     $paragraphs_types = ParagraphsType::loadMultiple();
  92.     /** @var \Drupal\paragraphs\ParagraphsTypeInterface $paragraphs_type */
  93.     foreach ($paragraphs_types as $paragraphs_type) {
  94.       $this->items[$paragraphs_type->id()] = [
  95.         'id' => $paragraphs_type->id(),
  96.         'label' => $paragraphs_type->label(),
  97.       ];
  98.     }
  99.  
  100.     if (!empty($this->exclude)) {
  101.       $this->items = array_diff_key($this->items, array_flip($this->exclude));
  102.     }
  103.  
  104.     return (new ArrayObject($this->items))->getIterator();
  105.   }
  106.  
  107.   /**
  108.    * {@inheritdoc}
  109.    */
  110.   public function count($refresh = FALSE) {
  111.     parent::count($this->items);
  112.   }
  113.  
  114. }
  115.  

As you can see in the gist above, the source plugin is very simple. It grabs a list of all available Paragraphs types and removes the types you would like to exclude.

The next step is to write a migration that updates the configuration for the form display.

  1. id: paragraphtypes_form_display__status
  2. label: Add status field to paragraph form display.
  3. source:
  4.   plugin: up_paragraphs_type
  5.   exclude:
  6.    - text
  7.   constants:
  8.     entity_type: paragraph
  9.     field_name: status
  10.     form_mode: default
  11.     options:
  12.       region: content
  13.       settings:
  14.         display_label: true
  15.       third_party_settings: {  }
  16.       type: boolean_checkbox
  17.       weight: 5
  18. process:
  19.   bundle: id
  20.   entity_type: constants/entity_type
  21.   field_name: constants/field_name
  22.   form_mode: constants/form_mode
  23.   options: constants/options
  24. destination:
  25.   plugin: component_entity_form_display
  26. migration_tags:
  27.  - up_paragraphstype
  28.  

The migration uses the new source plugin "up_paragraphs_type" and excludes the Paragraphs type "text" from the list to process. In line 11..17 we set exactly the same display settings as in the screenshot showing the configuration changes made for the "Text" Paragraphs type.

In the "process" section the migration loops over the results from the source plugin, using only the returned ID from each Paragraphs type and otherwise the constants defined earlier. Since we would like to update the form display configuration, we choose the "component_entity_form_display" plugin as destination, which is kindly provided directly by Drupal Core.

After running the migration all Paragraphs types available on the site are configured to display the "Published" checkbox. Yeah!

What about new fields?

But what about the new field I needed to create? Basically the migration doesn't really differ from the one above. The only thing we need to add is an additional migration creating the field configuration for each Paragraphs type.

Let's say, we would like to create a text field named "Comment" for all Paragraphs types. Then you will need to create the field storage for this field using something like this:

  1. id: paragraphtypes_field_storage_paragraphs_comment
  2. label: Define field storage for field_paragraphs_comment.
  3. source:
  4.   plugin: empty
  5.   constants:
  6.     entity_type: paragraph
  7.     id: paragraph.field_paragraphs_comment
  8.     field_name: field_paragraphs_comment
  9.     type: string
  10.     cardinality: 1
  11.     settings:
  12.       max_length: 255
  13.     langcode: en
  14.     translatable: true
  15. process:
  16.   entity_type: constants/entity_type
  17.   id: constants/id
  18.   field_name: constants/field_name
  19.   type: constants/type
  20.   cardinality: constants/cardinality
  21.   settings: constants/settings
  22.   langcode: constants/langcode
  23.   translatable: constants/translatable
  24. destination:
  25.   plugin: entity:field_storage_config
  26. migration_tags:
  27.  - up_paragraphstype
  28.  

Note: if you have created the field manually (like me for Paragraphs type "Text") you can skip this migration because the field storage is already existent.

To add the newly created field we need to create a field instance for each Paragraphs type. This can be done by using the migration destination "entity:field_config":

  1. id: paragraphtypes_field_paragraphs_comment
  2. label: Adds field_paragraphs_comment to paragraph types.
  3. source:
  4.   plugin: up_paragraphs_type
  5.   exclude:
  6.    - text
  7.   constants:
  8.     entity_type: paragraph
  9.     field_name: field_paragraphs_comment
  10.     translatable: true
  11.     label: 'Comment'
  12. process:
  13.   entity_type: constants/entity_type
  14.   field_name: constants/field_name
  15.   bundle: id
  16.   label: constants/label
  17.   translatable: constants/translatable
  18. destination:
  19.   plugin: entity:field_config
  20. migration_tags:
  21.  - up_paragraphstype
  22. migration_dependencies:
  23.   required:
  24.    - paragraphtypes_field_storage_paragraphs_comment
  25.  

Simple, isn't it?

What's up next?

After seeing how simple it is to create and update field configuration some new ideas came to our mind. It should also be possible to create entity types containing all required fields and display configuration using migrate. This could be a great option to enable some additional features on a site simply by clicking on a button (disclaimer: of course you need to export the configuration and eventually do some more stuff).

But this is stuff for another blogpost ...

Stefan Borchert
  • CEO

Stefan maintains several Drupal contributed modules, has been working on Drupal core since the early days, and is author of Drupal 8 Configuration Management (Packt Publishing).