How to update from Drupal 8 to Drupal 9
This article is based on real events
The Beta version of Drupal 9 has been available since March 2020. Now, as I write in June 2021, the version is perfectly stable and is at 9.1.9.
In November 2021, support is expected to end for version 8.x. So, it’s important to update before that day arrives.
There are several technical articles online about how to do the update, but I think this one may be the best: https://drupalize.me/tutorial/upgrade-drupal-9?p=2766
All in all, the changes from D8 to D9 are not too drastic and the upgrade shouldn't take more than a few working days. Of course, that will depend on the project, specifically on the contributed models (quantity and update degree) and the custom modules that we’ve developed (how much code is deprecated).The number of patches we have on our modules can also impact the process. In this sense, we recommend reviewing these patches to determine which are no longer necessary, either because the contributed modules have already solved the problem or because they’re core Drupal patches that have been fixed in Drupal 9.
What really changes from Drupal 8 to Drupal 9?
Most of the changes are minor, but here are the two main types of changes:
There are several methods that were deprecated in the latest version of Drupal 8 that won’t be available in Drupal 9.
Most of these methods are services or functions that have changed names. I’d also like to mention some changes in the .install type files, related to the use and access to tables and databases, which are now managed differently. We’ll go through all the most noteworthy changes a bit later on.
Update of the information about the module:
In MODULE.info files, an additional line should now appear indicating that the module is compatible with Drupal 9.
Update contributed models:
Contributed models must comply with the two aspects we just mentioned — not having deprecated code for the latest version of D8 and having their .info files updated.
Now, we’ll go over the process to update each contributed module.
- Check for a D9-compatible version
We’ll look on drupal.org for the module we want to update. For example, let’s consider the flag module. Once we find it in their code repository, we look for the most up-to-date branch. In this case, it’s: https://git.drupalcode.org/project/flag/-/tree/8.x-4.x
If we go to the .info file, we’ll see the line:
core_version_requirement: ^8.8 || ^9
That means this module, in this version, is compatible with Drupal 9.
We can also use this link: https://dev.acquia.com/drupal9/deprecation_status to check if the module we need is available for Drupal 9 or not.
Knowing this, we only need to indicate in composer.json the module’s branch. Since the branch of the example module is 8.x-4.x, we can call the version for this module “4.x” in composer, since the names usually correspond to the branches, ignoring the first part before the dash. Here, for instance, you leave out 8.x
- There’s not a D9-compatible version, but someone has created a Merge request
It’s possible that when we’re looking for a contributed model, we find that it isn’t compatible with D9. In other words, we can’t find a branch that supports this version.
If that’s the case, we can look for information about any open issues regarding the module’s compatibility with Drupal 9. For example, the Brightcove module currently has more than 100 open issues:
If we search for Drupal 9 or D9, we’ll usually find one or more issues related with the update.
You should also keep in mind that Drupal has something like a robot that automatically marks the contributed modules that aren’t updated for Drupal 9. That means, in theory, that all the contributed modules that aren’t correct should have an issue created named something along the lines of “Drupal 9 compatibility” or “Automated Drupal 9 compatibility fixes.”
We can then review these issues and see if a Merge Request (MR) has been made. We can also check to see what the merge requests look like, how old are they, who made them, etc. We can also check to see each of the merge requests (if there was more than one) and compare them to try and figure out which is the most complete.
In this case, we see that this MR is the best candidate: https://git.drupalcode.org/issue/brightcove-3146320/-/tree/3146320-D9-compatibility
Once we’ve identified it, we need to incorporate it into our composer.json file with the following nomenclature:
Here, we indicate the repository URL where the official module has been forked, and the reference to the concrete branch. We will also indicate a name (whichever one we want) in the parameter version to later call this version in our composer.json.
In our case, we usually name them: dev-BRANCHNAME.
Finally, we incorporate this module into the composer.json as we usually do:
- There's not a compatible version or a Merger Request
It’s possible that the contributed module only has an issue, but no MR and there hasn’t even been a user that’s commented on or indicated a possible patch for adapting the module to Drupal 9.
If that’s the case, we’ll have to do an update just like how we would with a custom module, with the only difference being that we’ll have to create the MR following the rules and processes of Drupal and its community.
- Create a fork
The first step is to create a fork. When we get to the page with the issue of the module we’re looking at, we’ll see a button that says “Create issue fork.” If you don’t see that, it’s because you have to login to drupal.org (and register) beforehand. It will suggest a name for us to use, and the best thing is to keep that name.
- Download and correct
We download the repo to our local computer and fix the Drupal 9 compatibility problems according to the instructions we will provide for updating deprecated code.
We can even include the module locally in our project to test it.
Once corrected, we commit & push and then send the Merge Request to the official module so they can include it as soon as possible. Don’t expect it to be approved immediately. My guess is that his type of Merge Request will begin to be approved in November.
- Include our version in compose
Just like how we did before, now we can include our own module fork in composer.json.
Update deprecated code:
As we’ve mentioned, the majority of the code errors that we’ll encounter when we change from D8 to D9 are small, like changing one line for another or changing a call to a service for a call to something similar with another name.
The most elegant way to detect these problems is to update our project to the latest version of D8 and use some of these modules to indicate which errors appear, like this, for example:
This library will detect which code will stop working in Drupal 9 and what we have to substitute for the update. The changes we make in our code will work in the latest version of Drupal 9 (to date).
Some common examples of errors that could emerge are:
- Call to deprecated constant NODE_PUBLISHED: Deprecated in drupal:8.?.? and is removed from drupal:9.0.0. Use \Drupal\node\NodeInterface::PUBLISHED instead.
- Call to deprecated method entityManager() of class Drupal: in drupal:8.0.0 and is removed from drupal:9.0.0. Use \Drupal::entityTypeManager() instead in most cases. If the needed method is not on \Drupal\Core\Entity\EntityTypeManagerInterface, see
the deprecated \Drupal\Core\Entity\EntityManager to find the correct interface or service.
- Parameter $entityQuery of method Drupal\your_module\EventSubscriber\FeedSubscriber::__construct() has typehint with deprecated class Drupal\Core\Entity\Query\QueryFactory: in drupal:8.3.0 and is removed from drupal:9.0.0. Use \Drupal\Core\Entity\EntityStorageInterface::getQuery() or \Drupal\Core\Entity\EntityStorageInterface::getAggregateQuery() instead.
- Call to deprecated function db_drop_index(): in drupal:8.0.0 and is removed from drupal:9.0.0. Instead, get a database connection injected into your service from the container, get its schema driver, and call dropIndex() on it.
- Call to deprecated function drupal_set_message(): in drupal:8.5.0 and is removed from drupal:9.0.0. Use \Drupal\Core\Messenger\MessengerInterface::addMessage() instead.
- Call to deprecated function db_change_field(): in drupal:8.0.0 and is removed from drupal:9.0.0. Instead, get a database connection injected into your service from the container, get its schema driver, and call changeField() on it
- Parameter $alias_manager of method Drupal\your_module\Form\customForm::__construct() has typehint with deprecated interface Drupal\Core\Path\AliasManagerInterface: in drupal:8.8.0 and is removed from drupal:9.0.0. Use \Drupal\path_alias\AliasManagerInterface.
- Parameter $temp_store_factory of method Drupal\your_module\Form\customForm::__construct() has typehint with deprecated class Drupal\user\PrivateTempStoreFactory: in drupal:8.5.0 and is removed from drupal:9.0.0. Use \Drupal\Core\TempStore\PrivateTempStoreFactory instead.
Therefore, most of the code updates will consist of following the recommendations in the drupal-check library. If we don’t understand exactly what to do from these alerts, we can look for more information in the official Drupal documentation (drupal.org) or in related forums.
The hosting requirements for Drupal 9 have also changed slightly. The main change is that now the minimum PHP version required is 7.3. In this link, you can see the rest of the hosting requirements:
We can update the hosting characteristics in our version of Drupal 8 directly so that it’s all ready for the change to Drupal 9.
Update Drupal core:
The last update is for our project’s Drupal core.
For that, we’ll use the option “--no-update” that allows us the composer to establish the versions we need for each module and library in our project, avoiding interdependencies that can lead to errors. An alternative option is to edit our entire composer.json, updating all our modules and taking into account all the dependencies.
Update of core-dev:
composer require drupal/core-dev:9.*@dev --dev --no-update --update-with-dependencies
Update of core-recommended and core-composer-scaffold:
composer require drupal/core-recommended:9.*@dev drupal/core-composer-scaffold:9.*@dev --no-update --update-with-dependencies
We run the update on the rest of the modules:
Note that we used 9.* as our Drupal version. This will allow composer to automatically establish the latest stable version. Once it’s established, we can manually set it in the composer.json, to, for example 9.1.9, or use * for minor updates (9.1.*).
Concepts to keep in mind
- If you have any questions about the process, we recommend that you go over one of the best articles on how to update from D8 to D9:
- We need to pay attention to the contributed modules and non-official branches that we’ve included in our project. At some point, those branches should be accepted by the people who maintain the code of these modules and then we’ll be able to include these modules directly without having to create a personalized fork in our composer.json
- Regarding the contributed modules that we (or someone else) have forked to make compatible with Drupal 9, it’s possible that the official module could update a functionality or fix a security issue or bug. If that’s the case, we should merge from that new official change in the module in our fork, as well as make sure that the module’s code remains compatible with Drupal 9.
- Finally, we should pay attention to the code we write from now on, so we don’t use functions, methods or classes that are incompatible with Drupal 9.