I have had quite a journey upgrading one of our client’s sites from Drupal 9 to Drupal 10. Needless to say, a few hours were used up just searching how to resolve different issues which I encountered. So I have put together this little guide to hopefully help you avoid the same headache.

The first thing to do is to install drupal/upgrade_status module in your project and enable it. composer require drupal/upgrade_status && drush en upgrade_status. After the module is enabled, go to the reports page: Reports > Upgrade Status. This page should contain all the upgrades and code changes necessary before your site can be upgraded to Drupal 10.

The following are things that I had to do on the particular site I was working on at the time, but your mileage may vary. Every site is different and may present its own unique challenges. You should only deploy the upgrade after fully testing the site, and remember to always make backups and have a restoration strategy ready before you deploy major changes to production.

  1. Upgrade Drupal core to latest 9.4 or 9.5, in order to upgrade to 10.

  2. Upgrade CKEditor 4 to CKEditor 5 by following this official guide. If you
    have custom CKEditor 4 plugins which don’t have an upgrade path to CKEditor 5, then you can keep using CKEditor 4 but as a contrib module. You may want to install this anyway, to avoid errors when you deploy the Drupal 10 upgrade, when the config import tries to uninstall CKEditor.

  3. Upgrade contrib modules to versions which support Drupal 9 and 10.

    • If the new version only supports D10, then you can add it to composer as an alternative version, for example: 'drupal/module_name': '^1 || ^2', and you can have version 1 still installed while on Drupal 9. Once you install Drupal 10, version 2 of the module will be installed by composer.
  4. Scan and fix custom modules for Drupal 10 deprecations and compatibility. This is not very complex, check all custom modules on the upgrade_status page and click “Scan selected”. Now you can click on the “X Problems” link to find out what code you need to fix. Mostly it’s just some entity query fixes, some deprecation fixes, and updating module info yml files to say this module supports Drupal 10. Note that if you have any disabled custom modules which are only enabled for certain situations, then you need to still enable it and fix the code deprecations during this process.

  5. Uninstall obsolete modules and themes like Color, RDF, Seven etc. You can’t remove core modules or themes, so simply uninstalling them should be enough. You may need to set your Admin theme to Claro and re-export your config on this step. Use drush theme:uninstall [themes] command (drush thun for short) to uninstall themes.

  6. Remove orphaned permissions which are still assigned to user roles. Export your config before starting this step. The upgrade_status module will tell you which permissions need to go from which roles. Simply edit the user.role.[role].yml config ymls and remove the relevant lines from the permissions array. Note that after this step you need to re-import your config to refresh what upgrade_status says.

For more information on the upgrade preparation, see the official documentation.

  1. Upgrade to Drupal 10. See documentation

    Specifics on Drupal 10 upgrade:

    1. Make sure you can get PHP 8.1 or later on your hosting provider. Otherwise you won’t be able to run Drupal 10 on your production server as Drupal 10 requires PHP 8.1 or later.

    2. If you have any D9 only modules with a D10 compatibility patch, make sure to install mglaman/composer-drupal-lenient before attempting to upgrade. Otherwise these modules will create a dependency problem. After the installation, you need to explicitly declare the D9 modules which should be treated as D10 modules. For example, to allow drupal/token to be installed run:

      composer config --merge --json extra.drupal-lenient.allowed-list '["drupal/token"]'

    3. If you host on Pantheon, note that they currently don’t support Drush 12. So you must install Drush 11 as a dependency. To do this run composer require drush/drush:^11. Note: You don’t need to update the pantheon.yml to say drush_version: 11, that will just get rejected on push. Pantheon’s drush will be ignored anyway if you have drush installed in your vendor folder.

    4. If you encounter dependency hell, and composer just won’t let you upgrade to D10, but you are sure that all the dependencies are in order, then the quickest thing to do is to delete the composer.lock file, delete vendor/ web/core web/modules/contrib web/themes/contrib web/profiles/contrib, then run composer install which will install from the composer.json file and create a new lock.

    5. Don’t forget to drush updb and drush cex after the upgrade. This means you should run the upgrade on top of an installed and functioning D9 database.

    6. If your custom theme is based on classy then don’t forget to install drupal/classy as it’s now a contrib theme.

    7. If you have Pathauto installed then you need to update some configs by hand. Namely pathauto.pattern.[name] config files have had their selection_criteria plugins updated. For example node_type becomes entity_bundle:node.

    8. If you use Entity Embed, you need to manually replace CKEditor Embed buttons (entity_embed module) with SVG icons.

    9. Update custom module/theme JS files to use core/once instead of core/jquery.once. See Remove jQuery dependency from the once feature on examples on how to do this.

    10. Re-run tests and fix any issues which may arise from API updates. If you encounter weird test issues, it may be worth upgrading to PHPUnit 10.

    11. If you have Migrate installed, and are planning to run migrations, then you need to upgrade Migrate Tools and Migrate Plus to version 6. If you use --group option of migrate import, then you also need to change migration configs to use tags, as the --group option no longer exists in 6.

    12. If you use weitzman/drupal-test-traits you should upgrade it to version 2 or later.

    13. If you have Drupal (Symfony) event dispatchers, the parameters of the Symfony\Component\EventDispatcher\EventDispatcherInterface::dispatch() method was updated to receive the Event object first, and the event name second. Before it was vice versa, so you simply need to reverse them.

Note that you can ignore some false positives such as an empty custom profile, which phpstan can’t scan, or suggestions to remove disabled modules which are situationally enabled.

The whole update process for the particular client’s site was very difficult, as it had lots of different modules installed, some even locked to specific dev version commit hashes, as well as some heavily patched old major versions of modules which needed to be upgraded, which meant we needed to rewrite our code to use the newer version of the module’s API. On top of that, the site was quite heavily customized and had lots of user-facing functionality.

But in the end, what took most of my time was the fact I’ve decided to introduce extra Javascript testing framework to our ddev environment and add a Javascript test to test the rewritten functionality. However, this test caused our CI jobs to time out during tests and never finish. The error of “Timed out” wasn’t particularly helpful, and I’ve even reached out to Travis support to find out what’s happening. In the end, after trying a bunch of things to no avail, and just before I started to tear my hair out, thankfully, I finally managed to figure out that the addition of Selenium and Chrome and this one Javascript test was the straw that broke the camel’s back; the Travis CI VM was running out of memory and starts swapping, which causes it to take more than 10 minutes to run a single test and therefore time out! Once this was clear, I needed just a couple of hours to reduce the memory footprint of our tests, and even managed to get the tests to run quicker!

In summary, this was by no means an easy task! And if you’re upgrading a site to Drupal 10, I hope this post helps you.

bboro's profile

Bysa Boro