Upgrading to Drupal 10 (And Beyond) With Composer

February 22, 2023 | Matthew Ramir
Upgrading to Drupal 10 (And Beyond) With Composer

Every iteration of Drupal brings a multitude of security improvements, accessibility improvements, and a host of new features created by the Drupal community. Drupal 10, released in December 2022, brings impressive UI improvements that give a beautiful refresh to the admin interface and default theme. 

Since the launch of Drupal 8 in August 2015, Drupal has continued to evolve and add new features that keep it in lock-step with modern development practices. One of the biggest changes that took root in Drupal 8 is the integration of Composer, a PHP-based dependency manager. Composer is a powerful tool that can be used to install modules, upgrade Drupal, and even check compatibility between modules and dependencies used across your project. Understanding how to leverage Composer to upgrade your Drupal codebase is an absolute necessity. Fortunately, Composer is easy to master. 

In this article, we will give a brief overview of how Composer tracks your project’s dependencies in composer.json and composer.lock. We will then discuss leveraging Composer and community-built tools to ensure compatibility between your code and the latest updates. Finally, we will cover using Composer commands to upgrade Drupal and address conflicts.

If you are running an older version of Drupal such as Drupal 7, check out this article for tips on upgrading your website from Drupal 7 to Drupal 8+. 

Key Composer Principles

Composer is a PHP-based dependency manager that is used to track and download things like Drupal modules, Symphony libraries, and even Drupal core itself. Composer uses semantic versioning to communicate compatibility between different package versions. A change in the major version (such as from Drupal 9.x to 10.x) means that there are potential incompatibilities that developers may need to address when upgrading a project. 

Composer uses a composer.json file to track our project’s explicit dependencies as semantic version ranges. For example, your project may require drupal/core:>=9.4 which tells Composer to install Drupal core version 9.4 or greater. Each one of the modules and themes used by your project will have a corresponding line in composer.json.

Each one of our project’s dependencies will have its own dependencies. For example, the Drupal Commerce module requires the Address module as well as a non-Drupal PHP library used for currency formatting. When we download the commerce module, Composer will also download the Address module and Currency formatting library. 

As Composer resolves each of these dependencies recursively, it builds a full list of 3rd party dependencies used across your project (including dependencies of dependencies).  The fully resolved dependency tree is then stored in the composer.lock file. This file contains the metadata of each dependency, including the location the package is downloaded from, any PSR-4 autoloading information, and a hash that references the exact version of a dependency. When other developers and CI pipelines run the composer install command, Composer reviews the composer.lock file and downloads all of the listed dependencies to your project.

Assessing Upgrade Compatibility

Incompatibilities can take a few different forms. In the case of Drupal 10, there were 8 modules, and a theme removed from core (some were moved to the contrib space, others deleted). We also see various deprecated functions removed from the core completely. If your project relied on any of these you will need to take action to resolve the incompatibilities.

The Drupal community maintains a module called “Upgrade Status” which can be used to give you a full picture of your upgrade path. This module gives you a series of tools that can be used to scan a Drupal installation for deprecated module usage. It also scans your codebase for calls to deprecated functions, or incompatibilities in info.yml and composer.json files.

# Install the latest dev version of Upgrade Status
composer require drupal/upgrade_status:4.x-dev --dev

One of the most powerful tools that comes with Upgrade Status is called  “Drupal Check”. Drupal Check uses static analysis with PHPStan under the hood to find calls to deprecated functions. Drupal-check is a great tool to help find errors in code in a performant manner, and is the type of tool you should have built into your CI pipeline. I would be remiss if I didn’t call out Acquia BLT, which has Drupal Check built in!

Upgrade Status has a dependency on Drupal Check. This means Composer will automatically install Drupal Check alongside Upgrade Status during the composer require command. We can also explicitly require just Drupal Check via a similar  composer require command.

# Require Drupal Check explicitly
composer require mglaman/drupal-check
# Run drupal-check
php vendor/bin/drupal-check  

Running Drupal Check will output a list of potential code issues for you to check and resolve before performing the upgrade. Any calls to deprecated code can be resolved using Drupal’s change records. Drupal’s codebase will also usually emit PHP notices containing details on how to resolve the deprecation moving forward. 

PHP Version Upgrades 

Drupal 10 requires a minimum of PHP 8.1, which is a far cry from the minimum of 7.4 required for Drupal 9. PHP 8 brings impressive optimizations and changes to language constructs. Composer allows us to specify PHP version as a project dependency in the same way we require other dependencies. This forces Composer to check whether our packages are compatible with PHP 8. 

composer require php:"^8.1"  --no-update

We are also able to require a specific version of PHP to be running on the host machine. This will give developers a heads-up if the PHP version running on their system hasn’t been upgraded to the appropriate version.

composer config platform.php 8.1

Most PHP 7.4 code is compatible with 8.x, but it is always a good idea to review backwards compatibility via change notices and the tools outlined above.

Upgrading Drupal Core With Composer

Finally, it's time to upgrade Drupal! Using Composer, we can explicitly require Drupal 10 using the composer require command. This will update our composer.json and add Drupal 10 as an explicit dependency. We also pass the --no-update flag to prevent the full dependency resolution process from running. This gives us a good spot to “save our work” before we embark on the upgrade process. 

composer require drupal/core:"^10.0" --no-update 

This require command tells Composer that the minimum acceptable version of Drupal core is 10.0. The package to place an explicit version requirement on will vary from project to project. If your project was set up using the instructions on Drupal.org, this version requirement would go on the drupal/core-recommended package. You can find the package to place the version constraint on by inspecting your composer.json file.

composer require drupal/core-recommended:"^10.0" --no-update 

Finally, we can upgrade all of our dependencies by running the command:

composer update

This command takes the information in our composer.json file and attempts to resolve the dependency tree. As it does this, it will analyze all your project’s dependencies, including PHP version, and determine if there is a way to satisfy all of your dependencies. This process usually isn’t straightforward, and you may end up with conflicts. This is ok! In fact, this means Composer is doing its job properly in spotting conflicts.

The conflict output will be something like the following:

- Root composer.json requires drupal/devel ^4.0 -> satisfiable by drupal/devel[4.0.0-rc1, ..., 4.x-dev].
- drupal/core-recommended[10.0.0-alpha4, ..., 10.0.0-alpha5] require symfony/var-dumper v6.0.8 -> satisfiable by symfony/var-dumper[v6.0.8].
- Conclusion: don't install symfony/var-dumper v6.0.8 (conflict analysis result)

In this case, Composer is telling us devel is causing a conflict with Drupal core on one of their mutual dependencies - symfony/var-dumper. After visiting the Devel module page we can see there is a newer version of devel that is compatible with Drupal 10. We can require the new version of this module, then re-run the update process.

 

composer require drupal/devel:^5.0 --no-update
composer update

This resolves the version incompatibility for the devel module, and we would continue on to resolve other conflicts posed in the update process. Resolving these conflicts may require a few different approaches. Sometimes it requires using the dev version of a module, other times it might require submitting your own patch to Drupal.org. The Upgrade Status module does a lot of this legwork for us, but understanding and resolving Composer conflicts is an important skill in modern Drupal development.

From here the process is rinse and repeat - run Composer update, analyze conflicts, and resolve with the appropriate approach. 

Historically, upgrading between Drupal versions has been fraught with challenges that require extensive development and testing. Since the introduction of Composer and community tools like Upgrade Status and Drupal Check, upgrading has become near trivial. With a little bit of configuration and know-how, Composer can be used to upgrade everything from PHP versions and contrib modules, to Drupal core itself,all while finding incompatibilities and potential issues.

Understanding and using Composer appropriately is the key to successful upgrades and maintenance of modern Drupal projects.