The first stable release for Drupal 9 shipped right on schedule — June 3, 2020. The Drupal 8.9.0 release was available the same day, and that means end-of-life for 8.7.x.

Since we all have migrated our sites from Drupal 7 to 8.9.x already (right??), it should be a fairly straightforward process to port everything from 8 to 9 when the time comes. This article covers what is involved with the 8 to 9 migration, sharing some of the gotchas we encountered in the hopes that you can have a smooth transition.

Are you familiar with what is coming in Drupal 9? How can you assess what is needed? How do you know what code needs to be updated? What other steps are involved?

This will help prepare you when it comes time to make the leap and to reassure you that this should be a straightforward and painless process.

Drupal 9

Drupal 9 is not being built in a different codebase than Drupal 8, so all new features will be backward-compatible. That is a significant departure if you recently went through a Drupal 6 to 7, or Drupal 7 to 8 migration. You won’t have to map content types and fields using migration modules or custom migration plugins and you won’t have to restructure your custom modules from scratch. This is really good news for companies and organizations who want to port sites before Drupal 8 end of life in November 2021 and who want to avoid or minimize the disruption that can come with a complicated migration.

In terms of what the code looks like, Drupal 9 will be the same as the last Drupal 8 minor release (which is set to be 8.9), with deprecated code removed and third-party dependencies updated. Upgrading to Drupal 9 should be like any other minor upgrade, so long as you have removed or replaced all deprecated code.

The Drupal.org documentation visualizes the differences between Drupal 8.9 and 9 with this image:

Drupal 9.0 API = Drupal 8.9 API minus deprecated parts plus third party dependencies updated

Upgrades

Symfony 3 -> 4.4

The biggest change for third party dependencies is the use of Symfony 4.4 for Drupal 9. Drupal 8 relies on Symfony 3, and to ensure security support, Symfony will have to be updated for Drupal 9.

Twig 1 -> 2

Drupal 9 will use Twig 2 instead of Twig 1 (Drupal 8). CKEditor 5 is planned to be used for a future version of Drupal 9; this issue references 9.1.x for the transition. Drupal 9 will still depend on jQuery, but most components of jQuery UI will be removed from core.

PHPUnit 6 -> 7

For testing, PHPUnit 7 will be used instead of version 6. The Simpletest API will be deprecated in Drupal 9 and PHPUnit is recommended in its place. If you have an existing test suite using PHPUnit, you might have to replace a lot of deprecated code, just as you will do for custom modules.

6 Month release schedule

Along the lines of how Drupal 8 releases worked, Drupal 9.1.0, 9.2.0, and so on, will each contain new backwards-compatible features for Drupal 9 every six months after the initial Drupal 9.0 release. The list of Strategic Initiatives gives a detailed overview of major undertakings that have been completed for Drupal 8 or are proposed and underway for Drupal 9. We might see automatic updates for 9.1, or drush included in core.

How can you assess what is needed to upgrade?

There are some comprehensive guides available on Drupal.org that highlight the steps needed for Drupal 9 readiness. A lot of functions, constants, and classes in Drupal core have been deprecated in Drupal 9.

Some deprecations call for easy swap-outs, like the example below:

Call to deprecated method url() of class Drupal\file\Entity\File. Deprecated in drupal:8.0.0 and is removed from drupal:9.0.0. Please use toUrl() instead.

You can see a patch that has been created that swaps out url() with toUrl() straightforwardly:

-  $menuItem['thumbnail_url'] = file_url_transform_relative($imageFile->Url());
+  $menuItem['thumbnail_url'] = file_url_transform_relative($imageFile->toUrl()->toString());

Some deprecations are more involved and do require some code rewrites if your custom modules are relying on the outdated code.

Example:

Call to deprecated function pagerdefaultinitialize() in drupal:8.8.0 and is removed from drupal:9.0.0. Use \Drupal\Core\Pager\PagerManagerInterface->defaultInitialize() instead.

There is an active issue in the Drupal core issue queue for this deprecation. Rewriting outdated code sometimes requires going through issue queue comments and doing some research to figure out how the core module has been reconfigured. Often it is easiest to look at the core code itself, or to grep for that function in other core modules to see how they have handled the deprecation.

This is how I ended up replacing the pagerdefaultintialize() deprecated function for the limit() method in our custom module:

use Drupal\Core\Database\Query\PagerSelectExtender;
+ use Drupal\Core\Pager\PagerManagerInterface;
+ use Drupal\Core\Pager;

class CountingPagerSelectExtender extends PagerSelectExtender {
  /**
   * {@inheritdoc}
   */
  public function limit($limit = 10) {
    parent::limit($limit);
     + /** @var \Drupal\Core\Pager\PagerManage $pagerManager */+ $pager_manager = \Drupal::service('pager.manager');

    if (empty($this->limit)) {
      return $this;
    }

    $this
      ->ensureElement();
    $total_items = $this
      ->getCountQuery()
      ->execute()
      ->fetchField();
     - $current_field = pager_default_initialize($total_items, $this->limit, $this->element);
     + $pager = $pager_manager->createPager($total_items, $this->limit, $this->element);
     + $current_page = $pager->getCurrentPage();
    $this
      ->range($current_page * $this->limit, $this->limit);
    return $this;
  }

How do you know what code needs to be updated?

Fortunately, as is usually the case with Drupal, there is a module for that! Upgrade Status

This contributed module allows you to scan all the code of installed modules. Sometimes a scan can take a while, so it might make sense to scan custom modules one by one if you want to step through your project. Upgrade Status generates reports on the deprecated code that must be replaced and can be exported in HTML format to share with others on your team.

If you are using a composer-based workflow, install Upgrade Status using the following command:

composer require 'drupal/upgrade_status:^2.0'

You might also need the Git Deploy contributed module as a dependency. Our projects did.

The Upgrade Status module relies on a lot of internals from the Drupal Check package. You can install Drupal Check with composer and run it if you want a quicker tool in the terminal to go through the codebase to identify code deprecations, and you don’t care about visual reporting or the additional checks offered by Upgrade Status.

Tools such as Upgrade Status and Drupal Check are extremely useful in helping to pinpoint which code will no longer be supported once you upgrade your project to Drupal 9. The full list of deprecated code was finalized with the Drupal 8.8.0 release in December 2019. There could be some future additions but only if absolutely necessary. The Drupal Core Deprecation Policy page goes into a lot more detail behind the justification for and mechanics of phasing out methods, services, hooks, and more.

@deprecated in drupal:8.3.0 and is removed from drupal:9.0.0.  
Use \Drupal\Foo\Bar::baz() instead.  
@see http://drupal.org/mode/the-change-notice-nid
The deprecation policy page explains how the PHPdoc tags indicate deprecated code

For the most part, all deprecated APIs are documented at: api.drupal.org/api/drupal/deprecated

Screen grab of the deprecated projects list
There are a lot of pages

Since so many maintainers are currently in the process of preparing their projects for Drupal 9, there is a lot of good example code out there for the kinds of errors that you will most likely see in your reports.

Check out the issues on Drupal.org with Issue Tag “Drupal 9 compatibility”, and if you have a few thousand spare hours to wade through the queues, feel free to help contributed module maintainers work towards Drupal 9 readiness!

Screen grab of the issue search page with filters

Upgrade Status note

My experience was that I went through several rounds of addressing the errors in the Upgrade Status report. For several custom modules, after I cleared out one error, re-scanning surfaced a bunch more. My first pass was like painting a wall with a roller. The second and third passes entailed further requirements and touch-ups to achieve a polished result.

What about previous Drupal releases?

Drupal 8 will continue to be supported until November 2021, since it is dependent on Symfony 3, which has an end-of-life at the same time.

Drupal 7 will also continue to be supported by the community until November 2021, with vendor extended support offered at least until 2024.

Now is a good time to get started on preparing for Drupal 9!