Updating to Drupal 9

Two weeks ago I tweeted about updating a website with 123 contrib and 59 custom modules to Drupal 9. When I sent that tweet I was just starting the update and all was going pretty smooth. After using the awesome Upgrade Status module, all the contributed modules and our custom modules showed they were ready. Since our team believes waiting only makes it harder to keep up, we decided to go for Drupal 9.1.0 while we were at it.

I want to start by saying that we have quite some test coverage for our custom code, and a lot of functional tests as well. I’m not sure how much time this saved, but even with the test coverage soms issues were tricky to find. That is also the reason I’m writing this. Partially to help teams that do not have the luxury of extensive test coverage, partially as a reminder that test coverage will eventually save time!

Update the code using composer

Updating all the modules using composer was a little tricky. The site contains a little over 50 core patches. Luckily most still applied but 17 of them had to be updated or rerolled. We also had to update PHPUnit and since we are using brianium/paratest we had to update to PHPUnit 9 to make all dependencies work. After we had all the code updated, the fun started.

Get the site running

The first thing that needs to be done to get a running site is make sure all service dependencies are correct. The deprecated entity.manager and path.alias_manager were the ones we found most.



We found quite a few places where we had overridden or extended core classes that still injected the old entity manager. If you find references to the old entity manager in your code, there is a good chance you need to replace it with the entity type manager. In some cases though, you need other services as well, so please pay close attention to check if the constructor has changed.


Some of the classes we had extended/overridden and constructors we had to change were:

  • NodeController
  • CommentDefaultFormatter
  • TermSelection
  • ContentEntityNormalizer

Fixing our custom blocks

Now that we had a running site, some of the blocks wouldn’t show up. We traced this to the following issues.


 * context = {
* “user” = @ContextDefinition(“entity:user”, label = @Translation(“User”))
* }
* context_definitions = {
* “user” = @ContextDefinition(“entity:user”, label = @Translation(“User”))
* }


$context = new ContextDefinition(‘entity:’ . $entity->getEntityTypeId())$context = EntityContext::fromEntityTypeId($entity->getEntityTypeId());

This mostly applied to #pre_render methods for our code, but we also had some custom #lazy_builder callbacks.

$build[‘#pre_render’][] = my_module_block_pre_render’;$build[‘#pre_render’][] = [MyModuleBlock::class, ‘preRender’];

Fixing issues found by our tests

Now that we had a “working” site, it was time to run the tests. The following list of deprecations/changes was harder to track down and we would probably only have found those issues after extensive user testing.


public function on404(GetResponseForExceptionEvent $event) {public function on404(ExceptionEvent $event) {






interface MyModulePluginInterface extends ConfigurablePluginInterface, PluginFormInterface, PluginInspectionInterface {interface MyModulePluginInterface extends ConfigurableInterface, PluginFormInterface, PluginInspectionInterface {



We had references to getCurrentUserId in the config of media/node base field overrides (eg core.base_field_override.media.image.uid.yml). These should be replaced. Other entity types might have them too.

default_value_callback: ‘Drupal\media\Entity\Media::getCurrentUserId’default_value_callback: ‘Drupal\media\Entity\Media::getDefaultEntityOwner’


 * “https://www.drupal.org/link-relations/create" = “/api/my-entity/{id}” * “create” = “/api/my-entity/{id}”


$response = $http_client->get(‘/rest/session/token’, [‘query’ => [‘_format’ => ‘hal_json’]]);$response = $http_client->get(‘/session/token’, [‘query’ => [‘_format’ => ‘hal_json’]]);

We had to change some of our tests for compatibility with the new PHPUnit version.



protected function onNotSuccessfulTest(\Throwable $t) {protected function onNotSuccessfulTest(\Throwable $t): void {

Thats it…

Even though most of these changes had change records, I was not fully aware of all these breaking changes and I’m also not sure how realistic it is to keep track of everything. In the end it took about 4 full days to get all our code updated, tests debugged and code fixed. Looking at the list, it all seems like a very manageable set of changes for a project of this size. I can imagine most sites will be a lot easier to update. I guess having a decent amount of test coverage and debugging a couple of issues is probably a good enough way to deal with the changes. I’m curious how other teams handle this, so if you have a good way to maybe prevent some debugging in the future, please let me know!

Freelance Drupal Developer. Passionate about Open Source. Loves working on challenging projects with talented people. Maintainer of Media in Drupal 8 core.