How to upgrade Drupal 8 to 9

In less than a month (to be precise on November 2nd), Drupal 8 will no longer be supported. If you still haven't upgraded your site to Drupal 9 you should do it as soon as possible, to keep it secure and to get new features.

There will be no vendor extended support program for Drupal 8 because it depends on Symfony 3 which will also reach EOL in November. And nobody really wants to fork the old Symfony 3 and provide security fixes.

This article will show you how to perform the upgrade, but keep in mind that you might encounter some issues not described here. In fact, I guess that there is at least a 50% chance that you'll encounter some issues during the upgrade process because every project is different.

1. Check custom code for deprecations

You can't just run composer update and expect everything to be fine. You must check custom and contrib modules and themes, and make sure they are 100% Drupal 9 compatible. To check the custom code for deprecations you should install drupal-check.

composer require mglaman/drupal-check --dev

To perform the deprecations check run the following command:

drupal-check web/modules/custom/

In this case, web/modules/custom/ is the path to custom modules. This could also be something like public_html/modules/custom/ so please make sure that you provide the correct path. Don't forget to check your custom themes as well!

The best-case scenario after running the command is to see this:

Drupal-check output
Drupal-check output

But you also might see something like this:

[10-Oct-2021 07:30:49 Australia/Sydney] PHP Warning:  Cannot declare class \PHPUnit_Framework_AssertionFailedError, because the name is already in use in /var/www/html/public_html/core/tests/bootstrap.php on line 192
Warning: Cannot declare class \PHPUnit_Framework_AssertionFailedError, because the name is already in use in /var/www/html/public_html/core/tests/bootstrap.php on line 192

[10-Oct-2021 07:30:49 Australia/Sydney] PHP Warning:  Class 'PHPUnit\Framework\Constraint\Count' not found in /var/www/html/public_html/core/tests/bootstrap.php on line 193
Warning: Class 'PHPUnit\Framework\Constraint\Count' not found in /var/www/html/public_html/core/tests/bootstrap.php on line 193

[10-Oct-2021 07:30:49 Australia/Sydney] PHP Warning:  Class 'PHPUnit\Framework\Error\Error' not found in /var/www/html/public_html/core/tests/bootstrap.php on line 194
Warning: Class 'PHPUnit\Framework\Error\Error' not found in /var/www/html/public_html/core/tests/bootstrap.php on line 194

[10-Oct-2021 07:30:49 Australia/Sydney] PHP Warning:  Class 'PHPUnit\Framework\Error\Warning' not found in /var/www/html/public_html/core/tests/bootstrap.php on line 195
Warning: Class 'PHPUnit\Framework\Error\Warning' not found in /var/www/html/public_html/core/tests/bootstrap.php on line 195

[10-Oct-2021 07:30:49 Australia/Sydney] PHP Warning:  Class 'PHPUnit\Framework\ExpectationFailedException' not found in /var/www/html/public_html/core/tests/bootstrap.php on line 196
Warning: Class 'PHPUnit\Framework\ExpectationFailedException' not found in /var/www/html/public_html/core/tests/bootstrap.php on line 196

[10-Oct-2021 07:30:49 Australia/Sydney] PHP Warning:  Class 'PHPUnit\Framework\Exception' not found in /var/www/html/public_html/core/tests/bootstrap.php on line 197
Warning: Class 'PHPUnit\Framework\Exception' not found in /var/www/html/public_html/core/tests/bootstrap.php on line 197

[10-Oct-2021 07:30:49 Australia/Sydney] PHP Warning:  Class 'PHPUnit\Framework\MockObject\Matcher\InvokedRecorder' not found in /var/www/html/public_html/core/tests/bootstrap.php on line 198
Warning: Class 'PHPUnit\Framework\MockObject\Matcher\InvokedRecorder' not found in /var/www/html/public_html/core/tests/bootstrap.php on line 198

[10-Oct-2021 07:30:49 Australia/Sydney] PHP Warning:  Class 'PHPUnit\Framework\SkippedTestError' not found in /var/www/html/public_html/core/tests/bootstrap.php on line 199
Warning: Class 'PHPUnit\Framework\SkippedTestError' not found in /var/www/html/public_html/core/tests/bootstrap.php on line 199

[10-Oct-2021 07:30:49 Australia/Sydney] PHP Warning:  Cannot declare class \PHPUnit_Framework_TestCase, because the name is already in use in /var/www/html/public_html/core/tests/bootstrap.php on line 200
Warning: Cannot declare class \PHPUnit_Framework_TestCase, because the name is already in use in /var/www/html/public_html/core/tests/bootstrap.php on line 200

[10-Oct-2021 07:30:49 Australia/Sydney] PHP Warning:  Class 'PHPUnit\Util\Test' not found in /var/www/html/public_html/core/tests/bootstrap.php on line 201
Warning: Class 'PHPUnit\Util\Test' not found in /var/www/html/public_html/core/tests/bootstrap.php on line 201

[10-Oct-2021 07:30:49 Australia/Sydney] PHP Warning:  Class 'PHPUnit\Util\Xml' not found in /var/www/html/public_html/core/tests/bootstrap.php on line 202
Warning: Class 'PHPUnit\Util\Xml' not found in /var/www/html/public_html/core/tests/bootstrap.php on line 202

[10-Oct-2021 07:30:49 Australia/Sydney] PHP Fatal error:  During class fetch: Uncaught PHPStan\Broker\ClassAutoloadingException: Class PHPUnit\Runner\Version not found. in phar:///var/www/html/vendor/phpstan/phpstan/phpstan.phar/src/Reflection/Runtime/RuntimeReflectionProvider.php:143

Fortunately, fixing this is quite easy. This happens because of conflicting dependencies in require-dev section of the project's composer.json file. In my case, I had to remove all packages listed there.

Drupal 8 - require-dev conflicts
Drupal 8 - require-dev conflicts

To remove all those packages just execute the following command:

composer remove behat/mink behat/mink-goutte-driver jcalderonzumba/gastonjs jcalderonzumba/mink-phantomjs-driver mikey179/vfsstream phpunit/phpunit symfony/css-selector

You might have more or less conflicting packages, so just make sure to remove all those packages required by the drupal/core-dev package.

If you need those dev dependencies (used to run tests), then the proper way to install them is to run the following command:

composer require drupal/core-dev:"^8.9.19" --dev

If you don't need them then skip installing drupal/core-dev metapackage.

Now execute the command for checking deprecations again. If drupal-check finds some deprecations then you must fix them, before proceeding further.

2. Check contrib code for deprecations

By now the majority of modules and themes are Drupal 9 compatible, but it's best if you confirm this for the set of modules and themes used in your project.

Either manually check all contrib modules and themes or install Upgrade Status module that will perform the check for you.

To manually check if a module is Drupal 9 compatible, go to the Reports -> Available updates, click on the module link and check if it's Drupal 9 ready.

Drupal 9 ready
Drupal 9 ready

Update all modules and themes to their latest versions. In some cases, you will have to update to the latest major version and in some cases, you will have to switch to the dev version.

For example, the Swiftmailer module version 1 is not Drupal 9 compatible, so you will have to update to version 2. Just as an example, to update this module edit your composer.json file and change the version constraint from "drupal/swiftmailer": "~1.0" to "drupal/swiftmailer": "~2.0". Now save the changes, and execute the following command:

composer update drupal/swiftmailer --with-dependencies

Another great example is the Admin Toolbar module. Version 1 of this module is not D9 ready. If you try to update the core you'll get the following error:

Problem 1
    - drupal/admin_toolbar is locked to version 1.27.0 and an update of this package was not requested.
    - drupal/admin_toolbar 1.27.0 requires drupal/core ^8 -> found drupal/core[8.0.0-beta6, ..., 8.9.x-dev] but it conflicts with your root composer.json require (~9.2.7).
Problem 2
    - drupal/admin_toolbar_tools is locked to version 1.27.0 and an update of this package was not requested.
    - drupal/admin_toolbar_tools 1.27.0 requires drupal/core ~8.6 -> found drupal/core[8.6.0-alpha1, ..., 8.9.x-dev] but it conflicts with your root composer.json require (~9.2.7).

So, before upgrading the core, upgrade the module to version 3 by changing constraint from something like this "drupal/admin_toolbar": "^1.21" to this "drupal/admin_toolbar": "^3.0".

This is also a good opportunity to remove all unused modules from your project, so do it!

3. Upgrade to 9.x

The final step is to use Composer to upgrade your project to Drupal 9. Edit your composer.json file and change the version constraint from ^8.x.x to ^9.2.7. Do this for both drupal/core and drupal/core-dev packages. Now run the following Composer command:

composer update drupal/core drupal/core-dev --with-dependencies

and then perform database updates by using Drush:

drush updb

If you don't see any Composer errors during the core update, then consider yourself lucky. In my case, I had to fix the following issue:

Problem 1
    - Conclusion: don't install symfony/process[v4.4.0-BETA2] | install one of symfony/process[v3.4.0, ..., v3.4.4] (conflict analysis result)
    - Conclusion: don't install symfony/process v3.4.0 (conflict analysis result)
    - Conclusion: don't install symfony/process v3.4.1 (conflict analysis result)
    - Conclusion: don't install symfony/process v3.4.2 (conflict analysis result)
    - Conclusion: don't install symfony/process v3.4.3 (conflict analysis result)
    - Conclusion: don't install symfony/process v3.4.4 (conflict analysis result)
    - You can only install one version of a package, so only one of these can be installed: symfony/process[v2.1.0, ..., 2.8.x-dev, v3.0.0-BETA1, ..., 3.4.x-dev, v4.0.0-BETA1, ..., 4.4.x-dev, v5.0.0-BETA1, ..., 5.4.x-dev, 6.0.x-dev].
    - drupal/core 9.2.x-dev requires symfony/process ^4.4 -> satisfiable by symfony/process[v4.4.0-BETA1, ..., 4.4.x-dev].
    - Conclusion: don't install symfony/process v4.4.0-BETA1 (conflict analysis result)

In my composer.lock file, the symfony/process package was locked to version v3.4.47, and Drupal 9 requires version ^4.4. The problem was that Drush requires consolidation/site-process which in turn requires symfony/process package in version 3.x. You can see this by executing the following command:

composer why symfony/process

The composer why command shows which package causes the given package to be installed.

Drupal 8 - Composer why symfony/process
Drupal 8 - Composer why symfony/process

The easiest solution to this problem is to remove Drush and then update the Drupal core to version 9.2 and then finally install Drush again.

Note about drupal/core and drupal/core-recommended

Instead of drupal/core you might want to require drupal/core-recommended package. This package guarantees that all of the dependencies from drupal/core will be included in your Drupal site at exactly the same version that was tested with the version of Drupal you are currently using.

Not using this package means that Drupal's dependencies may float up to more recent versions than were tested with Drupal, and that could sometimes introduce bugs in Drupal.

By using this package, you can be sure that you are deploying your site with a combination of core and dependencies that are verified to work according to the automated tests.

If you decide to use the recommended package, to update Drupal core in the future you will have to use the following command:

composer update drupal/core-recommended --with-dependencies

… and that’s it! Now go on and update some projects to Drupal 9. And if you need help with anything let me know.

About the Author

Goran Nikolovski is a creator, speaker, open-source contributor, web developer specialized in Drupal and DevOps enthusiast. He is the founder of this website and he enjoys sharing his knowledge and experiences.