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.

The Path Alias core subsystem has been moved to the “path_alias” module

https://www.drupal.org/node/3092086

\Drupal\Core\Path\AliasManagerInterface
@path.alias_manager
\Drupal\path_alias\AliasManagerInterface
@path_alias.manager

EntityManager has been split into 11 classes

https://www.drupal.org/node/2549139
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.

entity.managerentity_type.manager

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.

Plugins now use the ‘context_definitions’ key to define their contexts

https://www.drupal.org/node/3016699

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

Entity contexts have dedicated classes

https://www.drupal.org/node/2976400

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

Render callbacks must be a closure or implement TrustedCallbackInterface or RenderCallbackInterface

https://www.drupal.org/node/2966725
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.

The “Symfony\Component\HttpKernel\Event\GetResponseForExceptionEvent::getException()” method is deprecated since Symfony 4.4, use “getThrowable()” instead

https://www.drupal.org/project/drupal/issues/3113876

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

Several file uri/scheme functions deprecated and moved to \Drupal\Core\StreamWrapper\StreamWrapperManagerInterface

https://www.drupal.org/node/3035273

\Drupal::service(‘file_system’)->uriScheme($uri)StreamWrapperManager::getScheme($uri)

user.private_tempstore is deprecated use the ‘tempstore.private’ service instead

https://www.drupal.org/project/entity/issues/2951683

$container->get(‘user.private_tempstore’)$container->get(‘tempstore.private’)

ConfigurablePluginInterface is deprecated in favor of ConfigurableInterface, DependentPluginInterface.

https://www.drupal.org/node/2946161

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

Deprecate TermInterface::getVocabularyId()

https://www.drupal.org/project/drupal/issues/2850691

$term>getVocabularyId()$term>bundle()

The ::getCurrentUserId method is deprecated

https://api.drupal.org/api/drupal/core%21modules%21node%21src%21Entity%21Node.php/function/Node%3A%3AgetCurrentUserId/8.7.x
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’

POSTing to EntityResource can now happen at /node, /taxonomy/term … instead of /entity/node, /entity/taxonomy_term …

https://www.drupal.org/node/2737401

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

CSRF token route protection moved out of the REST module to be available to other core systems and contrib.

https://www.drupal.org/node/2772399

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

Forwards-compatibility shims of PHPUnit 8 functionality added for PHPUnit 6 & 7

https://www.drupal.org/node/3133050
We had to change some of our tests for compatibility with the new PHPUnit version.

$this->assertContains$this->assertStringContainsString

Overridden test methods require void return type hints

https://www.drupal.org/node/3114724

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.

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store