How to migrate from Drupal 8 to Drupal 9

Last modified
Tuesday, December 8, 2020 - 09:31

Yes, this topic might sound weird since migrating from Drupal 8 to Drupal 9 should be effortless, meaning is just a change of code base and voila! But what if your client is not happy with their current D8 site or making changes to and existing D8 site to improve performance can turn into a nightmare? or just as simple as Drupal 8 will drop support in less than a year and you want a better site structure when moving to Drupal 9 with the latest of the latest.
Fortunately, we still have the Migrate API available in Drupal 9 core and all the support modules that are available for migrating from old Drupal 7 or 6 are also available for Drupal 9 which makes this scenarios feasible. As a quick note, this post can help also if you are migrating to a fresh Drupal 8 from another Drupal 8.

In this post I am going to show you how to set up a fresh Drupal 9 site so you can import content from a existing Drupal 8 site, I'm going to be using a basic *.yml file to import files so you can take it as a start point for further and more complex migrations. This post assumes you have experience with migrations using the Migrate API since I won't be explaining basics such as creating a custom module, set up plugins, handle *.yml's migration scripts, drush migration commands, applying patches, etc.

First, we need to install a fresh Drupal 9 and enable the following core modules:

  • Migrate
  • Migrate Drupal

then we need to composer require the following modules contributed modules and enable them:

Quick note, Migrate Run is now the replacement for Migrate Tools for Drupal 9 since Drupal 9 needs Drush 9+ to work so this is very important to consider and a big plus of this module is that it uses the migrations core discovery plugin so you can store your scripts into the migrations/ folder of your custom module.

Currently there is an issue that needs to be patched in Migrate Run to avoid errors:

Once you have installed and enabled the above, we need to create a custom module to handle our migrations. Module structure should look like the following screenshot:

migrate mod folder sctructure

Where:

  • drupal8_migrate/ is the parent folder, my module.
  • migrations/ is the migrations folder where you'll be storing your migration scrips *.yml files.
  • migrations/migrate_files.yml an example of the above, we'll get to this later.
  • src/Plugin/migrate/process where your proceses are going to live.
  • src/Plugin/migrate/source where all your source plugins are going to live.
  • src/Plugin/migrate/source/ContentEntityAltern.php an example of the above, we'll get to this later.
  • drupal8_migrate.info.yml your info file, make sure you make it compatible with Drupal 9.

Don't forget to enable your custom module!

Currently Drupal 9 does have a ContentEntity plugin available in core that helps working with D8 or D9 entities but it does not have an out-of-the-box connection to external databases, here's where the this module becomes handy: Drupal 8 migration (source) it helps using the core plugin explained available for connecting to external Drupal 8 databases which is what we need to do in order to import content. By the time of the creation of this post, there is not release available of this module for Drupal 9, so you can either create a patch for that or follow the next steps in order to have it available on your Drupal 9, if you are planning on migrating from Drupal 8 to Drupal 8 the following steps are not necessary, just enable the module and jump to the legacy database step below.

  • Download the current available version of Drupal 8 migration (source), as of this date is the 8.x-1.0-beta1
  • Extract the content and find the ContentEntity.php file that is located under migrate_drupal_d8/src/Plugin/migrate/source/d8
  • Move this file to your custom module under drupal8_migrate/src/Plugin/migrate/source/ and rename it to ContentEntityAltern.php
  • Edit the file and update the annotation id and source to look like the following:
    /**
     * Base class for D8 source plugins to collect field values from Field API.
     *
     * @MigrateSource(
     *   id = "d8_entity_altern",
     *   source_module = "drupal8_migrate"
     * )
     */

     

Now that we have our source plugin configured we can start with the migration scripts. Don't forget to add the legacy or Drupal 8 source database definition on your settings.php, this is very important! so if you haven' already, edit your settings.php file and add:

/**
 * old Drupal 8 Site.
 */
$databases['drupal8_legacy']['default'] = [
  'database' => 'drupal8_site',
  'username' => 'my_db_user',
  'password' => 'my_db_user_pass',
  'prefix' => '',
  'host' => 'localhost',
  'port' => '3306',
  'namespace' => 'Drupal\\Core\\Database\\Driver\\mysql',
  'driver' => 'mysql',
];

Remember that $databases['drupal8_legacy']['default'] => drupal8_legacy is the Key for future migrations scripts!

Ok, now that we have everything we need to start with the actual migrations, let's create a yml script that will migrate all your public files from the old D8 to the fresh Drupal 9. Open your custom module and under the migrations/ folder create a new file called migrate_files.yml and paste the following:

id: migrate_files
label: Migrate public files from Drupal 8.
status: true
dependencies: {  }
migration_tags:
  - 'Drupal 8'
  - Files
source:
  plugin: d8_entity_altern # Our new source plugin. Note: use "d8_entity" if on D8 since the custom source plugin is not necessary.
  scheme: public
  key: drupal8_legacy # Our Drupal 8 source database name.
  entity_type: file
  constants:
    source_base_path: 'https://mydomain.com' # Source domain.
process:
  filename:
    -
      plugin: get
      source: filename
  source_public_absolute_path:
    -
      plugin: str_replace
      source: uri
      search: 'public://'
      replace: 'sites/default/files/'
  source_full_path:
    -
      plugin: concat
      delimiter: /
      source:
        - constants/source_base_path
        - '@source_public_absolute_path'
    -
      plugin: urlencode
  uri:
    -
      plugin: skip_on_file_not_exists
      method: row
      source: '@source_full_path'
      message: 'File field_name does not exist'
    -
      plugin: file_copy
      source:
        - '@source_full_path'
        - uri
  filemime:
    -
      plugin: get
      source: filemime
  filesize:
    -
      plugin: get
      source: filesize
  status:
    -
      plugin: get
      source: status
  created:
    -
      plugin: get
      source: timestamp
  changed:
    -
      plugin: get
      source: timestamp
  uid:
    -
      plugin: default_value
      default_value: 1
destination:
  plugin: 'entity:file'
migration_dependencies: null

You'll only have to update the source_base_path constant value to match a current working version of your Drupal 8 site so Migrate can read the files to be imported.

OK, that's it, you are ready to test out the migration by running the usual drush commands, you'll want to start by clearing caches:

$ drush cr
$ drush ms --tag="Drupal 8"

The above will show you the status of your new migration as shown in the screenshot:

migrate status

and finally, run the migration:

$ drush mim migrate_files
# If all goes well, you should see a message similar to:
[notice] Processed 131 items (131 created, 0 updated, 0 failed, 0 ignored) in 9.3 seconds (842.3/min) - done with 'migrate_files'

As for debugging, Migrate Devel is a great tool you can see data on your migration by running:

$ drush mim migrate_files --migrate-debug --limit=1 --update

this will get you an output similar to:

migrate devel output

And that is all! You can now visit your Files content page on your Drupal 9 site under /admin/content/files and you should see the newly created files.

Now let's turn these into Media Entities, since Media is now in core and the usage of this module is widely spread, importing files into Media is a process that goes hand to hand with the above. In order to migrate media, we need to create a new yml file where we will be handling the mapping for this kind of entities, go to your custom and under the migrations/ folder create a new file called migrate_media.yml and paste the following:

id: migrate_media
label: Media Files.
status: true
dependencies: {  }
migration_tags:
  - 'Drupal 8'
  - Files
source:
  plugin: d8_entity_altern # Our new source plugin. Note: use "d8_entity" if on D8 since the custom source plugin is not necessary.
  scheme: public
  key: drupal8_legacy # Our Drupal 8 source database name.
  entity_type: file
process:
  name:
    -
      plugin: get
      source: filename
  _file:
    -
      plugin: migration_lookup
      migration: migrate_files # Previously defined migration => migrate_files.yml
      source: fid
    -
      plugin: skip_on_empty
      method: row
  field_media_document: '@_file'
  field_media_video_file: '@_file'
  field_media_image: '@_file'
  status:
    -
      plugin: get
      source: status
  created:
    -
      plugin: get
      source: timestamp
  changed:
    -
      plugin: get
      source: timestamp
  uid:
    -
      plugin: default_value
      default_value: 1
  bundle:
    -
      plugin: static_map # for more the one media bundle.
      source: filemime
      default_value: image
      map:
        document/pdf: document
        application/pdf: document
        application/msword: document
        application/vnd.openxmlformats-officedocument.wordprocessingml.document: document
        application/vnd.ms-excel: document
        application/vnd.openxmlformats-officedocument.spreadsheetml.sheet: document
destination:
  plugin: 'entity:media' # Import to Media entities.
migration_dependencies:
  optional: {  }
  required:
    - migrate_files # Previously defined migration => migrate_files.yml

Clear your caches and after running drush ms again you should see the new definition in the list:

$ drush ms --tag="Drupal 8"

will show the new definition:

drush ms

Important: to run migrate_media.yml, you need to run migrate_files.yml first! this is very important since migrate_media.yml has migrate_files.yml as a dependency! So you can either run migrate_files.yml as described above or you can run both simultaneously:

$ drush mim migrate_files,migrate_media

# If all goes well, you should see a message similar to:
[notice] Processed 131 items (131 created, 0 updated, 0 failed, 0 ignored) in 8.7 seconds (906.1/min) - done with 'migrate_files'
[notice] Processed 131 items (131 created, 0 updated, 0 failed, 0 ignored) in 6.1 seconds (1280.2/min) - done with 'migrate_media'

If no errors found, you can now visit admin/content/media page and you'll see all your files turned into Media entities!

I hope this gives you a good starting point to run migrations and create more complex ones in order to move from Drupal 8 sites to new Drupal 9 sites!

Cheers!

Comments

Submitted by Willard on Mon, 01/11/2021 - 11:17

Permalink

When using this with D9 I also had to change the namespace to reflect where the new location of the php file was and in addition to renaming the file, I also renamed the class.

Submitted by Willard on Mon, 01/11/2021 - 11:21

Permalink

For the D9 migration, I had to change the namespace to reflect where ContentEntity.php was copied to as well as renaming the class along with the filename.

Add new comment

This question is for testing whether or not you are a human visitor and to prevent automated spam submissions.