Paul's Internet Landfill/ 2022/ Upgrading Drupal 8 to Drupal 9

Upgrading Drupal 8 to Drupal 9

Drupal 8 (aka D8) reached end of life last November. The KWLUG website was running Drupal 8, so it was time to upgrade.

I had heard that the Drupal 8 to Drupal 9 (aka D9) transition would not be as much work as our Drupal 6 to Drupal 8 had been. This was true, but the process was not as simple as composer update either. As always, Drupal is opinionated about "The Drupal Way To Do Things (tm)" and any deviations from the Drupal Way cause headaches. The official documentation was okay, but there were still gotchas that tripped me up. Furthermore, it was not always clear how I could contribute to the official documentation so future people would not get tripped up, so I am documenting things here.

Upgrade Status

The first step is to back up the production site and practice the upgrade process on development clones. Trying to do a major version upgrade on a production site is madness.

The Drupal documentation also recommended I install a package called Upgrade Status. I did this on a clone:

composer require drupal/upgrade_status
drush pm-enable upgrade_status

This created an administration menu which showed me the modules I could safely delete from the D8 site. It also analysed the D8 site and made other suggestions about my config files and custom modules.


I am still no fan of Composer, but it is the Drupal Way. Fortunately, I started with Composer near the beginning of our Drupal 8 experience. Unfortunately, the scaffolding Drupal uses for Composer changed during that time. The official upgrade documentation links to these instructions, which suggest making a lot of complicated changes to composer.json to make it compatible with Drupal 9. This approach did not work for me. I found it easier to clone the drupal/recommended-project templates and then carefully replace my project's composer.json with the cloned one. The composer.json was for Drupal 9.

"Carefully" is an important word here.

I had not changed any of my Drupal 8 composer.json other than to add dependencies. Apparently there are many customizations you can make to it, and obviously if you made such customizations you would have to carry over those changes too.

Here is a concrete example. In my original D8 composer.json I had:

    "require": {
        "composer/installers": "^1.2",
        "cweagans/composer-patches": "~1.0",
        "drupal/core": "^8.7",
        "drush/drush": "^8.0",
        "drupal/console": "~1.0",
        "drupal/migrate_plus": "^3.0",
        "drupal/admin_toolbar": "^1.18",
        "drupal/redirect": "^1.0@alpha",
        "drupal/migrate_upgrade": "^3.0",
        "drupal/riddler": "1.x-dev",
        "drupal/adaptivetheme": "^1.0@RC",
        "drupal/migrate_tools": "^3.0",
        "drupal/migrate_source_csv": "^2.0",
        "drupal/at_tools": "^2.0",
        "drupal/git_deploy": "2.x-dev",
        "drupal/views_fieldsets": "^3.3",
        "drupal/token": "^1.6"
    "require-dev": {
        "behat/mink": "~1.7",
        "behat/mink-goutte-driver": "~1.2",
        "jcalderonzumba/gastonjs": "~1.0.2",
        "jcalderonzumba/mink-phantomjs-driver": "~0.3.1",
        "mikey179/vfsstream": "~1.2",
        "phpunit/phpunit": "~4.8",
        "symfony/css-selector": "~2.8"

Everything inside the require-dev section went away (mostly because phpunit was causing a lot of problems). My new composer.json looked something like this:

    "require": {
        "composer/installers": "^1.9",
        "drupal/core-composer-scaffold": "^9.3",
        "drupal/core-project-message": "^9.3",
        "drupal/core-recommended": "^9.3",

        "drush/drush": "^11.0",
        "drupal/riddler": "^1.2",
        "drupal/adaptivetheme": ">=1.4.x-dev",
        "drupal/views_fieldsets": "^3.4",
        "drupal/token": "^1.10",
        "drupal/admin_toolbar": "^3.1",
        "drupal/redirect": "^1.7",

        "drupal/at_tools": "^3.3",
        "drupal/git_deploy": "^2.3"
    "require-dev": {

The first group (until drupal/core-recommended) came with the composer.json. The second group were the modules I wanted to maintain in the new site, including their currently supported versions. drupal/at_tools and drupal/git_deploy were things I no longer wanted in the site, but I kept them around until after the upgrade. If I got rid of them from composer.json without removing them using drush first, then drush updb database updates would fail.


I use the AdaptiveTheme theme on . This theme is simple and worked "well enough" for Drupal 8. In upgrading to Drupal 9 there was some drama.

Firstly, the AdaptiveTheme project was seemingly abandoned, and so was forked into at_theme. The original AdaptiveTheme wanted a package called at_tools but the fork needed a fork of at_tools called at_tool, which was incompatible with the original at_tools. What a mess!

Then I learned that the author of the forked theme gained control of the original version of AdaptiveTheme. So maybe I did not need to switch at all, and in fact the upgrade from whatever ancient version I was running to an updated version should have been fine... except for composer.json.

You see, the line in my original composer.json read:

        "drupal/adaptivetheme": "^1.0@RC",

which was okay except that the modern version of AdaptiveTheme was 5.1 and for some reason Composer would not upgrade my installed AdaptiveTheme with a new version. This broke other things, which ended up breaking the Drupal 9 upgrade.

My fix was to explicitly require a version of AdaptiveTheme greater than or equal to 1.4:

        "drupal/adaptivetheme": ">=1.4.x-dev",

This I guess worked? I believe this installed version 4.1 of AdaptiveTheme, which I then upgraded to version 5.1 later:

        "drupal/adaptivetheme": "^5.1",

(I wish I had exact error messages to show here, but it looks as if I did not record them. Sorry. You won't even find this web entry because of bad SEO, though.)

Also, pinning requirements using == is a terrible idea. I do not even love using ^ to pin major versions, but apparently that is The Drupal Way.

Custom Modules

The Upgrade Status module detected some small problems in my custom modules (mostly dpm and dsm debugging calls I had forgotten to comment out) but the big change was that I was supposed to add the following line to each of my *.info.yml files:

core_version_reqirement: "^8.7 || 9"

I was not going to redistribute these modules, so I cheated and allowed everything bigger than Drupal 8:

core_version_requirement: ">=8"

No doubt this will burn me in the future.

PHP Version and Drush

According to the official documentation, Drupal 9 requires PHP 7.3. However, there is a caveat: "Some individual modules may have specific requirements for PHP extensions and configurations beyond those listed below, so, please check the module's documentation as well." Sure enough, I got burned.

I do not know whether drush is still The Drupal Way, but I depend upon it pretty heavily and consider it a fundamental part of Drupal. Drupal 9 requires Drush 11, and Drush 11... depends on PHP 7.4 . So if you want to use Drush, you need to upgrade your PHP, or you get messages like:

  Problem 1
    - drush/drush[11.0.0, ..., 11.0.5] require php >=7.4 -> your php version (7.3.33) does not satisfy that requirement.
    - Root composer.json requires drush/drush ^11.0 -> satisfiable by drush/drush[11.0.0, ..., 11.0.5].

Drupal actually recommends PHP 8.0 or higher, but my webhost only provided up to PHP 7.4 . Fortunately I was able to switch my PHP version to that, and enable all the other PHP modules that Drupal requires.

The other headache (as always) is getting Drush into your $PATH. To do this I symlink Drush from the vendor folder:

ln -s vendor/drush/drush/drush ~/bin

but this is not the right way to do it, because it only works for one installation.

Drush Backups

drush archive-dump has been removed from Drush, because it does not back up the vendor or script directories. This is yet another casualty of Composer.

My workaround was to use drush sql:dump to grab the database, and then use plain old tar to grab the entire folder containing all of Drupal. Grabbing composer.json is good too.

In principle, if you have the following you should be able to restore the entire Drupal site:

Although this ought to work, I have not verified it.

Content Type Reinstallation

I made a big mistake when migrating my Drupal site from Drupal 6 to Drupal 8. I had a bunch of custom content types I wanted in the new site, so I made a custom module (kwlug_content_types) to store the YAML files for those types. In my custom module I had a config/install folder which installed all those content types.

In addition, I had some custom code that would interact with some of the custom types I created. I put this in the kwlug_content_types module as well.

Here is the problem. If I ever uninstall the kwlug_content_types module then I am hosed, because I cannot reinstall it -- if I try, then I get lots of errors about content types existing already. But if I do not reinstall the module then I lose my custom code.

My solution to this was a bad solution: I moved the YAML files from config/install to config/optional. Then if the content types already existed in the Drupal install they would not be overwritten. This is terrible because if some obsolete version of a content type existed already, it would not be fixed with the install.

I do not know The Drupal Way to handle this. I am thinking that maybe splitting the module into YAML files separate from the custom code may have been wiser.

Config Files

Before doing the upgrade I needed to make the web/sites/default folder user-writable, and then I needed to fix it.

In the settings.php I had to change $config_directories['sync'] to $settings['config_sync_directory'] . I got this information from the Lullabot upgrade tutorial.

Group Readership

In my development sites I had my files owned by my user account, but the web server being run as nginx. Thus I had to add nginx to my group account, but this caused lots of headaches. I needed to make lots of things group writeable, and then composer install overwrote those permissions and I needed to fix things again. There is probably some umask magic that would have fixed things, but I ran out of motivation before I figured it out. Fortunately this was not an issue on my production site.