The need to understand this feature of Drupal stems from client requests for the ability to migrate data from JSON:API to Drupal. This includes tasks such as creating nodes or entities and synchronising content based on the API. By having this capability, clients can store sensitive content on their own servers, keeping it private and under their control. They can conduct reviews internally and publish the content via APIwhen ready (either using Drupal contributed module: JSON:API, or end-point of their own), using the Migration module to transfer it to the production website. This workflow ensures complete protection against data leaks by avoiding storage on the production server. Additionally, it reduces the risk of errors during the editorial process, which is crucial in scenarios where clients do not fully trust their hosting provider and cannot afford any mistakes with their content.
Firstly, you’ll need to have a API servering as the data source of the migration, either:
Drupal JSON:API module: implements the JSON:API specification for Drupal entities, offering an opinionated approach that requires no configuration. It enables RESTful CRUD operations for the content of a Drupal site. (link)
Build your own API: build your own end-point, for instance, build an route that send a JSON response using Next.js. (link)
Use an Example API: free fake REST API you can find online, of instance:
As long as you have an API that provides data in JSON format with an array object representing the data in its body, you can continue on the journey of this tutorial, below is an example of the API using JSON:API module:
Secondly, you need to install the following modules:
Migrate: provides a flexible framework for migrating content into Drupal from other sources (a part of the core)
Migrate Plus: provides extensions to core migration framework functionality, as well as examples.
Migrate Tools: provides tools for running and managing Drupal migrations, including the GUI interface at /admin/structure/migrate.
You can do that executing the following in the rooter folder of the project:
1
2
3
4
5
6
7
8
# [install via composer]composerrequire'drupal/migrate_plus:^6.0'composerrequire'drupal/migrate_tools:^6.0'# [enable the modules]drushpm:enablemigratedrushpm:enablemigrate_plusdrushpm:enablemigrate_tools
Thirdly, after enabling the migration related modules, you will need to export your configuration, such that we can get the example .yml provided by the migration plus modules, as well as crafting our own migration task and import them back to drupal. You can either:
Manually Import/Export Configuration via Drush Utility:
Set configuration path in settings.php : $settings['config_sync_directory'] = 'config/sync';
Export and Import via: drush config:import, drush config:export
Configura Automatic Configuration Synchronisation on Manage > Configuration > Development > Configuration synchronization (or visiting url <root>/admin/config/development/configuration).
In all below section, you will need to perform at least one configuration import/sync whenever you make changes to the migrate_plus.migration.***.yml or migrate_plus.migration_group.***.yml file.
In the below example we’ll utiliize the migrate plus module to create two migration group: node and media to hold differnet type of migration task; As well as three migration tasks: standard page, media_release, image, and configure each task by definiing its source, target, and process in its migrate_plus.migration.***.yml file.
Firstly, let’s create two different MigrationGroup to hold the upcoming different type of migration task, we do that via first duplicating the example migrate_plus.migration_group.default.yml file provided by the migration plus module:
1
2
3
4
5
6
7
8
9
10
11
# [ migrate_plus.migration_group.default.yml ]uuid:4ab8a408-b785-4c3d-868f-dc7d028fb853langcode:enstatus:truedependencies:{}id:defaultlabel:Defaultdescription:'A container for any migrations not explicitly assigned to a group.'source_type:nullmodule:nullshared_configuration:null
We’ll create two groups via two different files:
1
2
3
4
5
6
7
8
9
10
11
# [ migrate_plus.migration_group.node.yml ]uuid:4ab8a408-b785-4c3d-868f-dc7d028fb851langcode:enstatus:truedependencies:{}id:nodelabel:'Node (Page)'description:'A container for any migrations relating to nodes (pages).'source_type:nullmodule:nullshared_configuration:null
1
2
3
4
5
6
7
8
9
10
11
# [ migrate_plus.migration_group.media.yml ]uuid:4ab8a408-b785-4c3d-868f-dc7d028fb859langcode:enstatus:truedependencies:{}id:medialabel:'Media (Docs/Images/etc)'description:'A container for any migrations relating to media (documents/images/etc).'source_type:nullmodule:nullshared_configuration:null
Later-on we can control the migration behaviour of all task within a group at once using the drush Command-Line utility, for instance:
drush migrate-import --group="node"
drush migrate-rollback --group="media"
drush migrate:reset-status --group="media"
(*p.s.
You may either leave the uuid field empty when you first create the file, and run dursh cim followed by drush cex, drupal will automatically assign the uuid for the group, or manually enter your uuid (please make sure no duplicate)
Once you are done creating the groups please remember to run drush cim to import these changes
Secondly, you’ll need to create the actual migration tasks via .yml file, create the following (you can use the example provided in this post or migrate plus module as starting point):
Migration Task: GovCMS Standard Page (Built-in Content Type)
# ====================================================================================# [METADATA]uuid:5c7ddbd6-cd67-472f-a185-28375206cd2e # UUID, please make sure no duplicatelangcode:en # Languagestatus:true# Enable / disable this migration taskdependencies:{}# Dependencies of this migration taskid:govcms_standard_page # The machine_name / unique_identifier of the taskclass:null# N/Afield_plugin_method:null# N/Acck_plugin_method:null# N/Amigration_tags:null# Migration Tagsmigration_group:node # Migration Grouplabel:govcms_standard_page # User friendly name (nick name)# ====================================================================================# [SOURCE (EXTRACT)]source:plugin:url # Url as the data sourcedata_fetcher_plugin:http # The data_fetching plugindata_parser_plugin:json # The data_parser normally limits the fields passed on to the source plugin to fields configured to be used as part of the migration. To support more dynamic migrations, the JSON data parser supports including the original data for the current row.include_raw_data:true# ↑ (Simply include the 'include_raw_data' flag set to `true` to enable this. This option is disabled by default to minimize memory footprint for migrations that do not need this capability.)headers:# HTTP reqeust headerAccept:application/json # ↑Content-Type:application/json # ↑urls: # Here we use the JSON:API get request as data source, read more:https://www.drupal.org/docs/core-modules-and-themes/core-modules/jsonapi-module/fetching-resources-get#s-get-article-media-entity-reference-field-image-url-uri-by-including-references- 'https://example-source/jsonapi/node/govcms_standard_page/?filter[title-filter][condition][path]=title&filter[title-filter][condition][operator]=CONTAINS&filter[title-filter][condition][value]=EXAMPLE-STANDARD-PAGE-'# (optionaly you can provide url in MigrateGroup like the following "shared_configuration: source: urls: 'https://www.omdbapi.com/?s=space&type=movie&r=json&apikey=86e4b169'item_selector:data # selector to get to the array of the data; You don't need this field if you JSON:API is an array from the root# --------------------------------------fields:# This is the fiels from the data source- # For each one you need to definename: source_data_id # [name]:the machine_name for this sourcelabel: 'Source - DataID' # [label]:user friendly name for this sourceselector: id # [selector]:selector to get to this field (after item_slector being applied)-name:source_drupal_internal__nidlabel:'Source - NodeID'selector:attributes/drupal_internal__nid-name:source_titlelabel:'Source - TITLE'selector:attributes/title-name:source_body_valuelabel:'Source - BODY VALUE'selector:attributes/body/value-name:source_body_formatlabel:'Source - BODY FORMAT'selector:attributes/body/format-name:source_body_processedlabel:'Source - BODY FORMAT'selector:attributes/body/processed# --------------------------------------ids:# This is the unique identifier for the individual rows/data-recordssource_data_id:# it has to be defined as one of the "fields" (in this examplle either: source_data_id, source_drupal_internal__nid, ...)type:string# ====================================================================================# [PROCESS (TRANSFORM)]process:# mapping between the "source fiels" and "destination fields"drupal_internal__nid:source_drupal_internal__nid # map [source_drupal_internal__nid] to [title]title:source_title # map [source_title] to [title]body/value:source_body_value # map [source_body_value] to [body/value \body/format:source_body_format # map [source_body_format] to [body/format] ← in case your field is made up of many sub-fieldsbody/processed:source_body_processed # map [source_body_processed] to [body/processed] /# ====================================================================================# [DESTINATION (LOAD)]destination:# The desitnationplugin:'entity:node'# (where data gets stored)default_bundle:page # the content type using ...# ====================================================================================migration_dependencies:{}
Migration Task: Media Release (Custom Content Type)
uuid:5c7ddbd6-cd67-472f-a185-28315206cd2flangcode:enstatus:truedependencies:{}id:imageclass:nullfield_plugin_method:nullcck_plugin_method:nullmigration_tags:nullmigration_group:medialabel:imagesource:plugin:urldata_fetcher_plugin:httpdata_parser_plugin:jsoninclude_raw_data:trueheaders:Accept:application/jsonContent-Type:application/jsonurls:- 'https://example-url/jsonapi/media/image?include=field_media_image&fields[file--file]=uri,url&filter[name]=NGINX-EXAMPLE-IMAGE-1'item_selector:datafields:-name:source_data_idlabel:'Source - DataID'selector:id-name:drupal_internal__midlabel:'Source - MediaID'selector:attributes/drupal_internal__mid-name:source_namelabel:'Source - NAME'selector:attributes/name-name:source_field_media_imagelabel:'Source - FIELD MEDIA IMAGAE'selector:attributes/field_media_image/related/hrefids:source_data_id:type:stringprocess:source_drupal_internal__nid:source_drupal_internal__nidname:source_name # ← Notice here instead of TITLE we are using NAMEdestination:plugin:'entity:media'default_bundle:imagemigration_dependencies:{}
Once the above configuration for task is created, like before, import it via the drush cim command, then you should be able to see it come up both in the admin backend (admin/structure/migrate) and dursh migrate:status printout:
(* some time you may get error of “failed to connect to database” or “migration id not found”, you’ll just have to run the configuration importation command again, and they seem to go away by themselves.
Execute Migration Import: you can import the configuration via clicking on the “execute” button on the admin backend (admin/structure/migrate) or via running drush migrate:import --group="node".
Rollback Migration (delete/revert node): you can roll back the changes via running drush migrate:rollback.