My former Drupal mentor, László Csécsy (boobaa) often says "If there is a task that you need to perform more than once and it can be easily automated then it should be automated."
We would like to introduce a new tool called Rector that could speed up an upgrade process and can reduce upgrade costs. We run a proof of concept for the possible use cases in which Rector can be beneficial for Drupal developers - including but not limited to the automated update to Drupal 9.
Drupal 9 is coming and according to the promises, the upgrade from Drupal 8 will be easy because Drupal 9 is built in Drupal 8.
What does "the upgrade to Drupal 9 will be easy" promise means precisely for developers? From the drupal.org post, Plan for Drupal 9:
Drupal 9 will simply be the last version of Drupal 8, with its deprecations removed.
Releasing Drupal 9 will only depend on removing deprecated functionality and upgrading Drupal's dependencies, such as Symfony.
The Drupal 8 change records show numerous classes and methods marked as "deprecated" in every release.
Depending on the size of a module's or a site's codebase, removing all deprecated APIs can require from a couple of development hours up to months. The process can be postponed until Drupal 9 is out, or it can happen in smaller batches after every Drupal 8 minor release. Either way, keeping a codebase up to date and ready for Drupal 9 upgrade requires resources.
Identifying deprecated API usages
Yes, there are already several different ways for identifying deprecated API usages in a Drupal 8 codebase. For example, if automated tests are available on a project, developers can run deprecation testing on the codebase. Other alternatives are static code analyzers like Matt Glaman's excellent PHPstan Drupal integration or PHPStorm's built-in code inspection.
All these tools are great, but they can only tell what deprecated API usages a codebase contains. They leave the actual work to developers: having to fix these depreciation issues one by one.
Applying the same changes several times is definitely a repetitive task. There is a PHP library that can help to automate these kinds of upgrade processes, it is called Rector.
What is Rector?
Rector is a reconstructor tool - it does instant upgrades and instant refactoring of your code.
Why is Rector awesome? Because it:
- uses AST (Abstract Syntax Tree) to parse PHP code instead of tokens 1,
- comes with built-in automation for upgrading to new PHP- or framework versions (ex.: Symfony, Laravel, etc.),
- can also automatically migrate your code from one framework to another or one test runner to another,
- can improve your code with automated code style and code quality fixes,
- is open-source.
If you would like to learn more about Rector before you continue, Tomas created an introduction series:
- Rector: Part 1 - What and How
- Rector: Part 2 - Maturity of PHP Ecosystem and Founding Fathers
- Rector: Part 3 - Why Instant Upgrades
Introducing Rector for Drupal 8
Rector already supported various frameworks, but it did not have support for Drupal 8 yet. This is the reason why Rector for Drupal 8 was born.
The Rector for Drupal 8 0.1.0 development version provides examples for the following possible use cases.
Instant code quality fixes and PHP 7.1 upgrade
Rector comes with numerous rector rules. Some of these rules are framework specific but most of them can run on any PHP code. The rules can be organized into prepared sets. Rector for Drupal 8 contains two prepared sets with all the built-in rectors that work on Drupal 8 codes. The drupal8 set only contains rectors that work on all PHP versions, including < PHP 7.0 versions that are still supported by Drupal core. For example: enforcing strict value comparison, normalizing conditions, removing dead code, etc. The drupal71 instantly updates any PHP 5 code to PHP 7.1 in a way that it still follows the Drupal 8 coding standards. I may include rectors in the future that make changes that require PHP 7.1 or above, ex.: add type to functions and methods parameters, etc.
Autocorrect framework specific mistakes and ensure best practices
All frameworks have their own list of best- and bad practices, so does Drupal 8.
In Drupal 8, it is a best practice to use "const" instead of "define" to define constants, because it is better for performance. The DefineToConstRector can fix all constant definitions according to this best practice.
As an example of bad practices, there are several traits in Drupal 8 that define a property and method with the same name. Probably the MessengerTrait is one of the most commonly used trait in Drupal core and contrib modules. Let's use this as an example.
$this->messenger property is not set by default, its value should be initialized in a class' constructor with Dependency Injection. If the property is set, then the
$this->messenger() method calls the injected Messenger service. If not, then it calls the global Drupal static service container, retrieves the Messenger service from that and sets the property's value. The problem with this default behavior is that it hides an issue caused by the uninitialized property. If the property is not initialized and a valid method is being called on the property, like
$this->messenger->addStatus() it causes a WSOD. Sadly, issues like this only reveal themselves in runtime. The only safe way to avoid this kind of issue is always using the method instead of the property.
The PropertyToMethodCallRector rector can automatically fix any code from this kind of common pitfall.
Get rid of deprecated code without writing code
Because Tomas already implemented various kinds of automated code upgrades and framework migrations, Rector already contains rectors that can be leveraged to remove deprecated Drupal 8 API usages from a codebase. They only need to be configured.
A built-in rector, called MethodNameReplacerRector, can automatically replace all usage of deprecated methods that used to generate a URL or a link for an entity. This rector can also ensure the
entityTypeManager object is used everywhere instead of the deprecated
entityManager object. Another common change in Drupal 8 core API is that global constants get replaced with class constants. The ConstToClassConstFetchRector rector that I created can replace all usage of a deprecated global constant with the new class constant.
The majority of Drupal 8 API changes are more complex than simple renames. Classes, interfaces, traits, and methods get deprecated and replaced by new ones. These are the types of architectural changes where Rector can demonstrate its real power. The deprecation of the UrlGeneratorTrait is one of the best examples for a significant architectural change. A trait with a protected property and non-static methods got replaced with a class with static methods. Properly removing UrlGeneratorTrait from a Drupal 8 codebase is a risky task because several steps need to be performed and therefore it requires great concentration from a developer. However, with Rector the complete replacement process can be automated, this is what UrlGeneratorTraitRector demonstrates.
I configured Travis CI to run Rector for Drupal 8 on several Drupal modules. The test builds include Drupal core modules (node, system) and contrib modules like Commerce or the Apigee Edge module. The following screenshots showcase some of the fixes that Rector for Drupal 8 applied on these modules.
Rector replaced method calls to deprecated methods from the UrlGenerator trait and also removed dependency to the trait:
Rector eliminated a possible bad flow caused by calling a method on a possible null property:
Rector replaced deprecated global constants with the new class constants:
What is next?
We hope this blog post gave you a good idea about the capabilities of the Drupal8-Rector project. Undoubtedly, its primary contribution to the Drupal community is the automated or at least semi-automated deprecated API removal from Drupal core and from contrib modules. The Rector library provides the framework and some good examples, but it requires a community effort to build new rectors or configure existing ones to address Drupal 8 core deprecations. There is still much work to do until all deprecated APIs since Drupal 8.0 are covered.
Please contribute to the Drupal8-Rector project if you see the potential in this proof of concept.