Recently, I had the opportunity to upgrade one of our projects from Drupal 8 to Drupal 9. In this blog post I would like to share some of the learnings I had while completing the upgrade. As you might expect, updating from Drupal 8 to Drupal 9 involves very little steps on the application layer. Most contributed modules are Drupal 9 ready and only a few exotic modules required me to work on a reroll of a Drupal 9 compatibility patch.

1. Keep track of Drupal 9 compatibility using Upgrade Status

To get started, I used Upgrade Status to analyse and keep track of the Drupal 9 readiness of the site.

It takes a while to scan all modules, but the UI is really helpful in identifying what is left for you to do. Follow these steps:

Run a full index from the command line:

drush us-a --all

Index individual projects:

drush us-a project_a project_b

You can access your upgrade report at yoursite.dev/admin/reports/upgrade-status.

2. Update to Composer 2

One fundamental step was to update to Composer 2. Refer to the documentation here. First we update composer itself:

composer selfupdate --2

If you have the composer version specified in your docker container, you might need to set it up there. In our case, we are using Lando, so let’s refer to the documentation on how to choose a composer version in Lando. In our lando.yml, we can explicitly specify the composer version as follows:

services:
  appserver:
    composer_version: 2

Updating to composer 2 may result in errors depending on the packages that you are using. When you run composer install, you might get an error like the following:

Your requirements could not be resolved to an installable set of packages.

Problem 1
    - Root composer.json requires wikimedia/composer-merge-plugin 1.4.1 -> satisfiable by wikimedia/composer-merge-plugin[v1.4.1].
    - wikimedia/composer-merge-plugin v1.4.1 requires composer-plugin-api ^1.0 -> found composer-plugin-api[2.0.0] but it does not match your constraint.

The according issue was just merged recently, but during the upgrade composer 2 support was only available via a fork of the original repository. In such a case, you can include a forked repository using the following approach. Add the following to your composer.json:

    "require": {
        "wikimedia/composer-merge-plugin": "dev-feature/composer-v2 as 1.5.0"
    }

    "repositories": {
        "wikimedia/composer-merge-plugin": {
            "type": "vcs",
            "url": "https://github.com/mcaskill/composer-merge-plugin"
        }
    }

3. Update to Drupal 9 and BLT 12 using Composer

We are using Acquia BLT to automate building and testing our Drupal sites.

Updating to Drupal 9 requires updating BLT to version 12. Make sure to follow the BLT 12 upgrade notes. Most importantly, some dependencies like PHPCS have been moved into their own plugins such as acquia/blt-phpcs. The following adaptations should be performed in composer.json:

{
    ...
    "require": {
        "acquia/blt": "^12",
        "cweagans/composer-patches": "~1.0",
        "drupal/core-composer-scaffold": "^9.1",
        "drupal/core-recommended": "^9.1",
    }
    "require-dev": {
        "acquia/blt-behat": "^1.1",
        "acquia/blt-drupal-test": "^1.0",
        "acquia/blt-phpcs": "^1.0",
        "drupal/core-dev": "^9",
    }
}

With the BLT update, some commands have changed. The BLT 11 versions of the commands, i.e.

blt validate:all
blt tests:all

Are now replaced with BLT 12 versions:

blt validate
blt tests

To perform the necessary updates, you need to run the following

composer update -w

Depending on your module dependencies, this might result in update errors. Follow the next sections for tips how to update your module dependencies for Drupal 9 compatibility.

4. Update contributed modules for Drupal 9

Because of the switch to support semantic versioning, modules might have changed their major release. For example devel has abandoned the 8.x-3.x series and uses now 4.x. You can always check the module page and verify that you find a version that requires Drupal ^9. Adapt the version in composer.json as follows:

{
    "require": {
        "drupal/devel": "^4.0",
    }
}

5. Notes on applying patches for module compatibility

Since drupal.org now supports issue forks & merge requests based on GitLab, .diff patch files might not need be available anymore within issues. You can still apply them using the following approach. Add “.diff” at the end of the merge request url. The following example illustrates how a merge request-based patch can be applied to a module in composer.json:

{
    "extra": {
        "patches": {
            "drupal/config_ignore": {
                "Support for export filtering via Drush (https://www.drupal.org/i/2857247)": "https://git.drupalcode.org/project/config_ignore/-/merge_requests/3.diff"
            }
        }
    }
}

When a module doesn’t state Drupal 9 as core_version_requirement or you need to have the composer.json to be added, you can use the following approach to include such a module using the composer workflow. You can use the module based on the version that is provided by the git branch that contains the fixes.

{
    "require": {
        "drupal/term_reference_tree": "dev-3123389-drupal-9-compatibility as 1.3-alpha3",
    },
    "repositories": {
       "drupal/term_reference_tree": {
            "type": "git",
            "url": "https://git.drupalcode.org/issue/term_reference_tree-3123389.git"
        }
    }
}

6. Update your custom code for Drupal 9 using Rector

Drupal 9 compatibility issues should be outlined by the Upgrade Status module mentioned previously. We are using drupal-check to automatically detect issues in the code base and this threw significantly more errors after the upgrade as code style requirements were increased. I used Rector to apply some automatic code style fixes for our custom modules. Rector wasn’t able to do all of them, so plan for some additional work here.

7. Working in multiple Lando instances of the same site

Because the Drupal 9 upgrade branch has a lot of dependencies that are different from Drupal 8, switching back and forth between branches might be cumbersome. I decided to run two instances in parallel, so that I don’t have to do full lando rebuilds.

Check out the same repository twice in two separate folders. Add and adapt the following .lando.local.yml within your second instance, so that you can run lando separately for both folders.

name: project_name_2

Use the following configuration to adapt url mappings, so that they don’t overlap with the original project.

proxy:
  appserver:
    - project_url_2.lndo.site
    - project_domain_2.lndo.site
  solr_index:
    - admin.solr.solr_index.project_2.lndo.site:8983

services:
  appserver:
    overrides:
      environment:
        DRUSH_OPTIONS_URI: "https://project_2.lndo.site"

In case you need have specified a portforward for the database, you should define a custom port for your second project instance

  database:
    portforward: 32145

Now you will be able to use lando start and respective commands within both project folders and access both site instances independently.

8. Conclusions

Thanks to semantic versioning, updating from Drupal 8 to Drupal 9 involves very little steps on the application layer. Most contributed modules are Drupal 9 ready and only a few exotic modules required me to work on a reroll of a Drupal 9 compatibility patch.

As you can see from the topics being mentioned, the effort to update the infrastructure certainly accumulates with upgrading from Composer 1 to 2, PHPUnit and making sure that other toolchain components are up to date.

Thank you Karine Chor & Hendrik Grahl for providing inputs to this post.