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 tweet by drubb 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.
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:
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 ...).
- <?php
- namespace Drupal\up_migrate\Plugin\migrate\source;
- use ArrayObject;
- use Drupal\Core\StringTranslation\StringTranslationTrait;
- use Drupal\migrate\Plugin\migrate\source\SourcePluginBase;
- use Drupal\migrate\Plugin\MigrationInterface;
- use Drupal\paragraphs\Entity\ParagraphsType;
- /**
- * Source plugin for ParagraphsType.
- *
- * @MigrateSource(
- * id = "up_paragraphs_type",
- * source_module = "up_migrate"
- * )
- */
- class UpParagraphsType extends SourcePluginBase {
- use StringTranslationTrait {
- t as t_original;
- }
- /**
- * List of paragraphs types to exclude.
- *
- * @var array
- */
- protected $exclude = [];
- /**
- * List of paragraph types.
- *
- * @var array
- */
- protected $items = [];
- /**
- * {@inheritdoc}
- */
- $options['context'] = 'up_migrate';
- }
- return $this->t_original($string, $args, $options);
- }
- /**
- * {@inheritdoc}
- */
- public function __construct(array $configuration, $plugin_id, $plugin_definition, MigrationInterface $migration) {
- parent::__construct($configuration, $plugin_id, $plugin_definition, $migration);
- $this->exclude = $configuration['exclude'];
- }
- }
- /**
- * {@inheritdoc}
- */
- public function fields() {
- return [
- 'id' => $this->t('ID'),
- 'label' => $this->t('Label'),
- ];
- }
- /**
- * {@inheritdoc}
- */
- public function getIds() {
- $ids['id']['type'] = 'string';
- return $ids;
- }
- /**
- * Return a comma-separated list of paragraph type ids.
- */
- public function __toString() {
- }
- /**
- * {@inheritdoc}
- */
- protected function initializeIterator() {
- $this->items = [];
- $paragraphs_types = ParagraphsType::loadMultiple();
- /** @var \Drupal\paragraphs\ParagraphsTypeInterface $paragraphs_type */
- foreach ($paragraphs_types as $paragraphs_type) {
- $this->items[$paragraphs_type->id()] = [
- 'id' => $paragraphs_type->id(),
- 'label' => $paragraphs_type->label(),
- ];
- }
- }
- return (new ArrayObject($this->items))->getIterator();
- }
- /**
- * {@inheritdoc}
- */
- }
- }
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.
- id: paragraphtypes_form_display__status
- label: Add status field to paragraph form display.
- source:
- plugin: up_paragraphs_type
- exclude:
- - text
- constants:
- entity_type: paragraph
- field_name: status
- form_mode: default
- options:
- region: content
- settings:
- display_label: true
- third_party_settings: { }
- type: boolean_checkbox
- weight: 5
- process:
- bundle: id
- entity_type: constants/entity_type
- field_name: constants/field_name
- form_mode: constants/form_mode
- options: constants/options
- destination:
- plugin: component_entity_form_display
- migration_tags:
- - up_paragraphstype
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:
- id: paragraphtypes_field_storage_paragraphs_comment
- label: Define field storage for field_paragraphs_comment.
- source:
- plugin: empty
- constants:
- entity_type: paragraph
- id: paragraph.field_paragraphs_comment
- field_name: field_paragraphs_comment
- type: string
- cardinality: 1
- settings:
- max_length: 255
- langcode: en
- translatable: true
- process:
- entity_type: constants/entity_type
- id: constants/id
- field_name: constants/field_name
- type: constants/type
- cardinality: constants/cardinality
- settings: constants/settings
- langcode: constants/langcode
- translatable: constants/translatable
- destination:
- plugin: entity:field_storage_config
- migration_tags:
- - up_paragraphstype
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":
- id: paragraphtypes_field_paragraphs_comment
- label: Adds field_paragraphs_comment to paragraph types.
- source:
- plugin: up_paragraphs_type
- exclude:
- - text
- constants:
- entity_type: paragraph
- field_name: field_paragraphs_comment
- translatable: true
- label: 'Comment'
- process:
- entity_type: constants/entity_type
- field_name: constants/field_name
- bundle: id
- label: constants/label
- translatable: constants/translatable
- destination:
- plugin: entity:field_config
- migration_tags:
- - up_paragraphstype
- migration_dependencies:
- required:
- - paragraphtypes_field_storage_paragraphs_comment
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 ...