One of the major selling points of Drupal 9 is that the upgrade is really easy: fix your deprecated code, update your contrib modules, run some composer magic, and you’re done. While that usually is the case, some sites have their own set of hacks, oddities, and technical debt that make the upgrade process a lot more arduous. If you’ve found yourself in the middle of one such upgrade then you’ve come to the right place. This blog post isn’t a complete guide on how to upgrade to D9; it’s supplemental to the existing documentation found here.
Make a plan, take it slow
First, you need to figure out what changes need to be made to the site for D9 compatibility and then build a plan around those requirements. The main things you’ll need to consider are local environment requirements, live environment requirements, deprecated contrib/custom code, and composer changes. It’s tempting to do all of these things in one fell swoop, but I recommend against that. Given how many different systems you’re going to mess with it’s really easy to get stuck in various composer or git knots and end up having to redo a lot of work. Also, it’s much easier to QA several small PRs than one enormous one.
I recommend starting with the environment requirements in the official docs which lists Apache/Nginx, PHP, Database, and Drush. Depending on your situation, I’d suggest the following addendums to this list:
- Use PHP 7.4 instead of 8. At time of writing it has the most consistent compatibility with contrib modules.
- Upgrade to Drush 10 and install it with composer if it’s not already: composer require --dev drush/drush.
- Upgrade to Composer 2. It will run much faster and be far less painful to work with during the upgrade process.
Once you’ve figured out what you need, make a PR that upgrades all of this for both your local and live environments. Drupal 8 will still work with all of these settings, and having them in your repo before moving to the next step in the process will make the whole thing much easier.
Contrib and custom code
Next you’ll want to figure out the code changes you need. The Upgrade Status module does a great job of listing out all of the deprecations in your various custom and contrib modules and provides both a UI and drush commands. Important note: if you just run composer require drupal/module_name you’ll automatically get the latest version of the module. If you need a specific version instead, use composer require drupal/module_name "1.2.3". This part should be relatively easy; however, one snag worth mentioning is the awkward situation when a contrib module you need isn’t up to date. If all the module needs is that core_version_requirement: ^8 || ^9 line in its .info file, then the composer lenient endpoint will allow you to apply the requisite patch and make the upgrade (Note: you need to be on Composer 2 to use lenient). If none of that works you could just copy the module code into your custom modules and make the changes yourself, but this should be a last resort. Once you’ve wrangled your code, I recommend committing two separate PRs, one for contrib work and one for custom for the same reasons we made the previous step into its own PR.
Applying the upgrade with Composer
With your environment and code up to date, you can follow the official docs to apply the D9 upgrade. Essentially this process boils down to running composer require 'drupal/core-recommended:^9' and composer update and enjoying your newly upgraded site. However, this assumes that Composer isn’t being the inscrutable eldritch beast that it devolves into from time to time.
I told you to remove var-dumper… why are you updating it?
Before tackling these issues, it’s important to have a clear understanding of composer’s elements and how they interact. The `composer.json` file is primarily a list of all the packages (and their versions) that make up your site. Keep in mind that this list is technically incomplete in that the listed packages have their own dependencies. The `composer.lock` file stores both sets of dependencies and information about them. This file also operates as the source of truth for every version of every package and will yell at you if you try to install something that conflicts. Both of those files are just json, though, and the actual code gets installed in several places:
- Drupal core goes to `/web/core`
- Contrib modules go to `/web/modules/contrib`
- Non-drupal libraries that you specifically pull in will usually go to `web/libraries` (e.g ckeditor libraries)
- Most everything else ends up in `/vendor`
Your goal with this part of the D9 upgrade is simply to create a composer.json and composer.lock file that can cleanly apply to your site and upgrade it to D9 with all of your required packages intact.
Sometimes composer doesn’t do what you expect it to, or it gives you unclear error messages. For example, I recently had a project that gave me the following error when I attempted to upgrade to Drupal 9.
I would expect errors saying certain modules are out of date, but what is drupal-driver? It’s not in the `composer.json` file, so it can’t just be updated or removed, but something is pulling it in and it is, in turn, preventing the upgrade. So what can be done? Before diving into such a question, it’s important to be set up for success. Consider using a global version of composer. While virtualization tools like Docker and Lando are great, they add a lot of overhead and make composer run a lot slower. If you’re running the same composer version (composer --version) in both your virtual environment and globally, it should generate the exact same results.
Next, learn more composer commands and use them to figure out why composer is acting weird. At the very least you should use the following composer tricks:
- --dry-run (composer require option) Simulate the command without actually making any changes. This is really useful for testing the waters without actually committing to a change that might be incorrect
- --no-update (composer require option) Very similar to `dry-run` in that it doesn’t actually install the package or its dependencies, but it does add them to your `composer.lock` and `composer.json` files
- why-not (command) Tells you what packages conflict with what you’re trying to install
- why (command) Tells you why a package is required
- --with-dependencies (composer update option) Update a package’s dependencies as well
So, with all that information lets try to understand why drupal-driver is blocking the upgrade. By running composer why drupal/drupal-driver we see that it is required by `drupal/drupal-extension` which is actually on the `composer.json` so now we can either upgrade or remove that package which will allow us to move on with the upgrade.
Ideally, with these options and deft use of the core composer commands (install, require, remove, update) you can finish the upgrade. However, if it's still not working, don’t be afraid to get a little hacky if you need to. Remember, all you need to do is provide functional `composer.lock` and `composer.json` files–how you get there is up to you. Here are some hacky tactics that I’ve found useful:
- If a specific package is giving you trouble, consider deleting it yourself at the source in the vendor or libraries directory
- If a package’s requirements are an issue, consider altering or removing its entry in the composer.lock file
- If everything is hopelessly confused, you can usually reset back to zero by resetting to your previous commit and running composer install. If that STILL doesn’t work you can do a harder reset by also manually deleting your `/vendor`, `/core`, and `/libraries` directories as appropriate.
Conclusion, aka Plan for Complications
Ultimately, this post exists to help folks avoid a trap that I fell into a couple of times. Assume the Drupal 8 to 9 upgrade is going to be complicated. Get the scope of the change first, and make a plan to address it. Split the work into separate PRs for each step and release them one at a time. Get familiar with the intricacies of composer, and don’t be afraid to get a little hacky if you need to. Happy upgrading!