Jump-Start Your Drupal 9 Code Updates with Drupal Check and Drupal Rector

Blake and I recently started the process of planning, and preparing, to migrate Drupalize.Me to Drupal 8 9. The primary component of our evolving infrastructure is a Drupal 8 site. With Drupal 9 right around the corner I wanted to know what it would take for us to upgrade that application.

Part of figuring that out is assessing all of our custom module and theme code to make sure it's compatible. Which, mostly means making sure we're not using an deprecated APIs. Drupal 9's API will be the same as Drupal 8.9's API with the deprecated code removed. If you remove any use of deprecated features, and your module still works on Drupal 8 it'll work on Drupal 9, too.

Image showing Drupal 8.9 API represented as a stack of blocks with some marked as deprecated alongside Drupal 9 API with the deprecated blocks removed.

Slide from State of Drupal 9 by Gabor Hojtsy.

To start figuring out what we need to change, and what it's going to take, I've been playing around with two helpful tools that I've been hearing a lot about lately:

  • Matt Glaman's Drupal Check (drupal-check)
  • and, Offer Shaal's Drupal Rector (drupal-rector)

What is Drupal Check?

Drupal Check can scan for use of @deprecated code and suggest a fix, as well as optionally perform static analysis. This makes it easier to locate the parts of your codebase that will need to be updated in order ensure Drupal 9 compatibility.

Example deprecated code output

------ -------------------------------------------------------
Line   lehub.module
------ -------------------------------------------------------
403    Call to deprecated method l() of class Drupal:
        in drupal:8.0.0 and is removed from drupal:9.0.0. Use
        \Drupal\Core\Link::fromTextAndUrl() instead.
------ -------------------------------------------------------

Install drupal-check

Install drupal-check in an existing project as a development dependency using Composer:

composer require mglaman/drupal-check --dev

Note: Drupal Check needs to be run from the root directory of a Drupal project in order to work.

Verify the install

Use this command to verify the install worked and view available options:

./../bin/drupal-check --help

Run Drupal Check

Here's an example of running drupal-check against a specific module (lehub is our custom module):

# The path to the executable may differ depending on your Composer configuration.
# Another common location is vendor/bin/drupal-check.
./../bin/drupal-check -ad modules/custom/lehub/

What is Drupal Rector?

About a month ago I posted on Twitter asking what people were interested in learning about in preparation for Drupal 9.

And one of the things I learned about was the Drupal Rector project from Offer Shaal (@shaal), and others. So I decided to give it a try. You can find the code here https://github.com/palantirnet/drupal-rector.

Rector is a PHP project that given another PHP codebase can apply a set of rules that refactor the PHP and output an updated codebase. A simple example would be renaming a class, and finding all the places in the codebase where the class is used and updating those as well. A more complex example would be automating the update from PHP 5.3 to PHP 7.3.

Drupal Rector can also automate the removal of deprecated code and perform other refactoring required to make a Drupal 8 module ready for use with Drupal 9. It's a wrapper around the Rector project that provides rules covering the most commonly-used deprecated code in Drupal. It doesn't cover everything yet. But it does cover the most common examples of deprecated code use and in many cases that might be all you need.

Either way, it's a great way to get a jump-start on ensuring your custom Drupal modules are Drupal 9 ready.

Note: Using drupal-rector can cause changes to your codebase. I recommend you do this in a Git branch so you can easily view a diff of the changes, and revert anything that doesn't work.

And ... Running drupal-rector requires a minimum of PHP version 7.2. I initially tried using PHP 7.1 and got some cryptic errors related to PHP autoloading. Switching to a newer version solved the problem.

Install drupal-rector using Composer

composer require --dev palantirnet/drupal-rector

Configure rector

Create a rector.yml configuration file. I created this in the root directory of my Drupal project, alongside the vendor/ and web/ directories. My codebase looks like this:

+-- README.md
+-- bin
+-- composer.json
+-- composer.lock
+-- ...
**+-- rector.yml**
+-- ...
+-- vendor
+-- web

Here's an example rector.yml file. You'll want to adjust the path to the drupal-8-all-deprecations.yml, and Drupal code directories like web/core to make sure they match your project's layout. These paths should be relative to the rector.yml file.

imports:
    - { resource: "vendor/palantirnet/drupal-rector/config/drupal-8/drupal-8-all-deprecations.yml" }

parameters:
    autoload_paths:
    - 'web/core'
    - 'web/core/modules'
    - 'web/modules'
    - 'web/profiles'

    file_extensions:
    - module
    - theme
    - install
    - profile
    - inc
    - engine

services: ~

This tells the rector PHP application to use the rules contained in the drupal-rector project, where to look for your project's code for referencing, and what file extensions (in addition to .php) it should operate on.

Execute drupal-rector

Now you can execute drupal-rector with:

./bin/rector process web/modules/custom/{MODULE} --dry-run

And it'll output examples of all the changes that would be made if you ran it without the --dry-run command.

2) web/modules/custom/lehub/src/LehubConfigOverrider.php

    ---------- begin diff ----------
--- Original
+++ New
@@ -71,7 +71,7 @@
        // If they exist, load any overrides defined by that consumer.
        // But... we can't use the entityTypeManager here, or we'll have
        // an infinite loop.
-      $consumer_data = db_select('consumer_field_data', 'cfd')
+      $consumer_data = \Drupal::database()->select('consumer_field_data', 'cfd')
            ->fields('cfd')
            ->condition('id', $this->consumer, '=')
            ->execute()
    ----------- end diff -----------

Applied rules:

    * DrupalRector\Rector\Deprecation\DBSelectRector

If you're happy with what you see, go ahead and remove the --dry-run flag and let 'er rip.

What about a UI?

Prefer a UI solution instead of the CLI? Gabor has a great video showing how to use the Upgrade Rector module to produce a diff via Drupal's UI.

The right fix might be a little harder

One thing I noticed when running drupal-rector is that in some cases while the suggested fix would technically work, it's maybe not the technically correct approach. The resulting code will work. And in many cases working is good enough. However, this might also be a good time to revisit this custom code and verify that in addition to removing use of deprecated features we're also using current best practices.

Take the above example for instance which suggests replacing calls to db_select() with \Drupal::database()->select(). This doesn't account for the fact that this code exists as part of a custom service definition. And the more technically-correct approach in this case would be to pass the database service to the LehubConfigOverrider service via an argument in lehub.services.yml.

In the little that I've used drupal-rector thus far the place where I've seen this opportunity for refactoring come up the most is when replacing calls to deprecated functions like db_select() or drupal_set_message() with calls to \Drupal::* convenience methods in scenarios where dependency injection should be used instead.

Conclusion

Both drupal-check, and drupal-rector, are useful tools to add to your toolkit. Especially when it comes to starting the work of ensuring your Drupal 8 code is compatible with Drupal 9. They can't do everything for you, but what you'll end up with is a comprehensive list of updates you'll need to make, and a sizeable head start in doing so.

Comments

Upgrade Status is built on top of the internals of drupal-check and includes a whole lot more checks, like twig deprecation checking, hook_theme uses and definitions, deprecated Drupal library uses and extension, info file checking, composer.json checking, etc. It also has drush commands. It does sit into your site as a module, which you can avoid with drupal-check, but most addon checks do require a bootstrapped Drupal, so that cannot be avoided.

Add new comment