If you have a Drupal 7 site, you likely recognize these directory paths:
sites/all/modules/contrib
sites/all/modules/custom
The first is where your site's various contributed ("contrib") modules are found and the second has custom modules that your team has coded over the years. If you're lucky enough not to have any custom modules, congratulations!
It's what's in the second directory path – sites/all/modules/custom
– that is the topic of this blog post. Specifically, what to do with your Drupal 7 custom modules.
Step 1: Inventory your Custom Code
Broadly speaking, for each of your custom modules you can ask the following three questions:
- Do I really need it?
- Can I replace it with an existing
contrib
orcore
module? - Is this functionality important enough to refactor?
Do you really need it?
Let's face it, some high-priority custom functionality from years ago may no longer be needed. Do you really need the custom module you built ages ago that counts the number of virtual tacos handed out to your team? If not, you can safely leave this custom module out of your modern Drupal plans.
Can you replace it with an existing contrib
or core
module?
Keeping with the counting virtual tacos example, if you want to keep the functionality, perhaps there are contrib
modules already in modern Drupal that have you covered. Okay, okay, there is no contrib
module for counting virtual tacos, but there may be a contrib
module that suits your needs. Don't reinvent the wheel with custom code when a contrib
module is available that you could use and support! Explore what Drupal contrib
has to offer.
Is this functionality important enough to refactor?
If you answer "yes" to this question, you can move on to the next steps below.
Step 2: Run the Module Upgrader Report
I recently wrote about the Drupal 7 to 8/9/10 Module Upgrader module and described it as, "...a great starting place to determine a module's readiness for modern Drupal compatibility."
After you have copied your Drupal 7 custom module into a vanilla installation of modern Drupal, a drush command will analyze the code and create a report, highlighting places in your D7 code that need attention for compatibility in modern Drupal.
Let's go a bit further than that last blog post and see what running a report and acting on it looks like in real life. In this case, I found one of our Drupal 7 custom modules called "Firewall Link". It was built to recognize any links having a certain class name and instead of taking the user directly to the expected page, a modal appears to warn the user that the link will only be accessible to users behind a corporate firewall.
Once I had copied the Drupal 7 module code into my vanilla modern Drupal installation's modules folder at web/modules/firewall_link
, I ran drush dmu-analyze
.
>drush dmu-analyze firewall_link
Indexing...
done.
An HTML report called upgrade-info.html
was created in the firewall_link
folder. From the screenshot below, you see that it has recognized several Drupal 7 functions – drupal_add_js()
, l()
, variable_get()
– that have been removed and will not work in modern Drupal. It helpfully links to documentation about the change and tells you the file and line number where the code was found.
Step 3: Decide what to do with your Custom Code
The report's results tell you where to look and that itself is a great start. Now you need to figure out what to do with that code to keep your current functionality running in modern Drupal.
For example, here is a screenshot of the report's expanded first finding: "drupal_add_js()
has been removed."
As per the finding, I check line 63 of the affected file and see that it is running in hook_init()
, a function that in Drupal 7 runs on every page load, but has been removed in modern Drupal. (The code snippet below is representative of what I found in the original code, but has been slightly altered.)
Drupal 7 Code
/**
* Implements hook_init().
*/
function firewall_link_init() {
// Add to Drupal.settings.
$settings = array(
'firewall-link' => array(
'heading' => t('This is a protected page'),
'message' => t('This page is only accessible within the corporate firewall. Proceed if you are logged in to the network.'),
'class' => 'firewall',
),
);
drupal_add_js($settings, 'setting');
}
I see from the above code that key:value pairs are passed into the settings
property of the Drupal 7 JavaScript Drupal
object (Drupal.settings
). This is a way to make those key:value pairs available to the site's front-end JavaScript.
Drupal.settings
has been replaced in modern Drupal by the drupalSettings
object, and hook_init()
has been removed, so to refactor this for modern Drupal, one option is to replace hook_init()
with a similar hook that is not removed – say hook_preprocess_page()
– and try something like this.
Code Refactored for Modern Drupal
/**
* Implements hook_preprocess_page().
*/
function firewall_link_preprocess_page(&$variables): void {
// Add values to drupalSettings.
$variables['#attached']['drupalSettings']['firewallLink'] = [
'heading' => t('This is a protected page'),
'message' => t('This page is only accessible within the corporate firewall. Proceed if you are logged in to the network.'),
'class' => 'firewall',
];
}
One great thing about using the Drupal Module Upgrader is that the analysis of the Drupal 7 custom module (ie. drush dmu-analyze firewall_link
) is done in your vanilla instance of modern Drupal, meaning that your old code is already in new Drupal and you can adjust it in place until it works! You just need an .info.yml
version of D7's .info
file to install the module and start testing. Once installed, try to load a page and you'll likely come across errors.
For instance, my first page load returned with a white screen of death (WSOD):
Call to undefined function variable_get() in firewall_link_preprocess_html()
Oops, my D7 module has calls to variable_get()
that need to be removed, but I'll work on that later. To fix the current WSOD, I commented out the offending line of code and then my page refreshed successfully. Inspecting the page's JavaScript using my browser console showed that drupalSettings.firewallLink
exists and has values, meaning my refactored code from above works.
Using this approach you can run your D7 code, see what breaks and then fix it, remove it, or refactor it, as necessary. Keep at it until you have a working, modern Drupal version of your module.
What I've outlined above depends on manual labor to refactor your custom D7 module. However, the Module Upgrader also has a command, drush dmu-upgrade
to automatically update your code to be modern Drupal-compatible. It scans your D7 code and does what it can to get it ready for modern Drupal. For example, I ran the command and it automatically created the firewall_link.info.yml
file, as well as a firewall_link.services.yml
file and even created InitSubscriber.php
, an Event Subscriber to take the place of the removed D7 hook_init()
method that I mentioned above! If nothing else, that's a great starting place for refactoring.
Retrofit for Drupal
One alternative solution to immediate all-out refactoring is a project called Retrofit for Drupal. Retrofit is intended to lessen the "all-at-once" burden of moving from Drupal 7 to modern Drupal, allowing developers to get things running first and then converting to the modern Drupal APIs. From the project's README
page:
Retrofit provides compatibility layers for legacy Drupal code to run on any version of Drupal.
If that sounds too good to be true, you can at least be certain of the project's maintainer, Matt Glaman. Matt is a Drupal stalwart and has been a guest on Chromatic's Drupal 7 End-of-Life podcast. In a blog post he outlines Retrofit's approach and features.
I tested Retrofit with my partially refactored D7 custom module and found that it works as advertised. For instance, the calls to variable_get()
that caused WSOD errors above now run successfully. In essence, it allows D7 code to run in modern Drupal without immediately breaking. I wrote "without immediately breaking" because I did eventually run into a problem with my module's settings form. The code for that broke because Retrofit didn't have a wrapper for D7's system_settings_form()
function, resulting in a "Call to undefined function" error. Keep in mind that Retrofit was at version 0.1.4
which in the world of semantic versioning means it's still in "initial development". Mr. Glaman is not super-human and cannot possibly conjure a 100% working solution during his initial development! Once I removed the offending function call, the settings page loaded. Color me impressed.
The Best of all Possible Worlds?
After trying both the Drupal 7 to 8/9/10 Module Upgrader module and Retrofit, I see how they could be used together to get your custom D7 module ready to be upgraded. The Drupal 7 to 8/9/10 Module Upgrader's report points out the trouble spots that need to be refactored, and Retrofit allows much of your existing D7 module's code to work in modern Drupal while you work on the refactor. It's a big bonus if the Module Upgrader can automatically transform your D7 code and give you a great starting point!
In attempting this approach I came across the complication that, at the time of writing, the Drupal 7 to 8/9/10 Module Upgrader uses core_version_requirement: ^8 || ^9
(works with Drupal 8/9) while Retrofit requires Drupal 10 and above. To deal with this I ran the two projects locally on different virtual hosts, the Module Upgrader running on Drupal 9 and Retrofit running on Drupal 10. After getting my Module Upgrader report and automatically generated files from the Drupal 9 installation, I copied them to the Drupal 10 instance with Retrofit and successfully continued refactoring.
So, yes, there's manual work for us to do but what did we expect? It's too much to hope that there exists a comprehensive, automated solution to refactor every instance of custom code from Drupal 7 to Drupal 10, but the tools that have been created are impressive. Follow the above steps and you can stop thinking about how to do the work and get on with actually doing it!
Roadmap Your Drupal 7 Transition
We’re offering free 45 minute working sessions to help you assess your organizations level of risk, roadmap your transition plan, and identify viable options!
Drop us a note, and we’ll reach out to schedule a time.