Table of contents
- Why do custom modules break during a Drupal migration?
- Which parts of a custom Drupal module break first?
- 1. Procedural hooks and the service container
- 2. The database layer
- 3. Variables, settings, and configuration
- 4. Theme functions and templates
- 5. Custom data storage and update paths
- How do you audit a custom Drupal module before migrating?
- Should you port the module or rebuild it?
- How much of the migration can be automated?
- How do you test a migrated custom module?
- What does a safe migration workflow look like?
- Final thoughts
- Frequently Asked Questions
- How long does it take to migrate a custom Drupal module?
- Can Drupal Rector fix my custom module automatically?
- Do Drupal 7 custom modules work in Drupal 10 or 11?
- Is it cheaper to rebuild a custom module or port it?
- What happens if I keep running custom modules on Drupal 7?
- Which tools check if my custom code is ready for the next Drupal version?
Custom modules are where Drupal migrations get interesting. Contributed modules usually have an upgrade path someone else has already built. Your custom code has no such luck. It was written for one site, by developers who may have moved on, against APIs that have probably since been replaced. Custom Drupal module migration is the process of porting or rebuilding site-specific code so it runs on a newer major version of Drupal.
This blog walks through what actually breaks in complex custom modules during a migration, how to find the damage before it finds you, and how to decide between porting and rebuilding.
Key Takeaways
- Custom modules break during migration because Drupal 8 rewrote the core APIs. Procedural hooks, db_query(), the variables system, and PHPTemplate theme functions all changed.
- Audit before you touch code. Upgrade Status and PHPStan (via drupal-check) tell you exactly what will fail and how much effort the fix needs.
- Drupal Rector automates a large share of the work. Rector rules now cover more than 80% of deprecated API uses in the Drupal 12 cycle (Drupal.org, 2026).
- Every module gets one of three verdicts: port it, rebuild it, or rebuild the architecture while keeping the logic. The audit tells you which.
Why do custom modules break during a Drupal migration?
Drupal 8, released in November 2015, was a full architectural rewrite of the core and its APIs (Drupal.org). Everything since builds on that foundation. So a Drupal 7 module isn't ported so much as translated into a different dialect of Drupal.
Later jumps are gentler. Moving from Drupal 10 to 11 mostly means clearing deprecated API calls, since majors now share an architecture. The Drupal 7 leap remains the hard one.
The stakes are real. As of September 2024, roughly 40% of the 728,784 tracked Drupal sites were still running Drupal 7 (Drupal.org usage statistics), and many of those sites stayed put precisely because of complex custom code.
Which parts of a custom Drupal module break first?
Five areas cause most of the pain. Here they are, roughly in order of how often they surface in an audit.
1. Procedural hooks and the service container
Drupal 7 modules live in .module files full of hooks and global function calls. Modern Drupal expects object-oriented code: services, plugins, event subscribers, and dependency injection. hook_menu() alone splits into routing YAML, controllers, and separate hooks for menu links and permissions.
The mechanical translation is learnable. The trap is business logic tangled across a dozen hooks with shared global state. That logic has to be untangled before it can be rewritten.
2. The database layer
Direct db_query() calls and db_select() chains are gone. The replacement is an injected database connection or, better, the Entity API. Modules that define their own tables via hook_schema() can still do so, but any query that touches core tables will find those tables changed.
3. Variables, settings, and configuration
variable_get() and variable_set() no longer exist. Their jobs are split across the Configuration API, the State API, and settings.php overrides. Choosing the right home for each value matters because config can be deployed between environments, but state cannot.
4. Theme functions and templates
PHPTemplate and theme() function calls gave way to Twig and render arrays. A custom module that prints markup directly needs its output layer rebuilt, not patched.
5. Custom data storage and update paths
Modules that store their own data need a migration plan for that data, separate from the code port. Core's Migrate API handles this well, but someone has to write the source plugins and mappings. This step is the one teams most often underestimate.
How do you audit a custom Drupal module before migrating?
Start with the Upgrade Status module. It scans your custom code against the target Drupal version and reports every incompatible API use, with severity. The Drupal Association's own Project Update Bot uses Upgrade Status for its analysis (Drupal.org, 2026), which says something about its reliability.
Pair it with PHPStan and the drupal-check wrapper for static analysis in CI. These tools catch deprecated calls on every commit, so the audit stays current instead of rotting in a spreadsheet.
Then read the numbers honestly. A module with 30 deprecation notices and clean architecture is a porting job. A module with 300 notices and logic nobody can explain is a rewrite wearing a porting job's clothes.
Should you port the module or rebuild it?
Port when the module's purpose is still valid, the code maps cleanly to modern APIs, and the audit shows contained damage. Rebuilding working logic for its own sake burns the budget.
Rebuild when the audit shows structural problems, when half the module's features are no longer used, or when a contributed module now does the same job. Ten years is enough time for contrib to catch up to your custom feature. Check before you port.
There is a middle path worth naming: rebuild the architecture, keep the logic. The hooks become services, but the algorithms and business rules move over largely intact. For complex modules, this is often the realistic answer.
How much of the migration can be automated?
More than you'd guess. Drupal Rector applies automated code fixes for deprecated API uses, and its rules covered 95% of deprecated Drupal APIs as far back as the Drupal 9 cycle (Matt Glaman, 2021). For the Drupal 12 cycle, coverage stands at more than 80% of newly deprecated API uses (Drupal.org, 2026).
Run Rector, review the diff, run your tests. The tool handles the repetitive renames and signature changes so your developers can spend their attention on the logic Rector can't touch: the tangled hooks, the custom storage, the decade of accumulated workarounds.
One caveat. Rector fixes deprecations between modern majors far better than it bridges the Drupal 7 gap, where the changes are architectural rather than mechanical.
How do you test a migrated custom module?
Write tests before the port, not after. Even a handful of kernel and functional PHPUnit tests capturing current behavior gives you something objective to check the ported module against.
Then compare outputs. Run the old and new modules against the same content and diff the results, whether that's rendered markup, API responses, or database state. Discrepancies found here cost hours. The same discrepancies were found in the production cost on weekends.
The cost of skipping this is measurable. Since Drupal 7's end of life in January 2025, several new vulnerabilities have been disclosed every month in widely used Drupal 7 contributed modules. Untested custom code carries the same class of risk with none of the public disclosure.
What does a safe migration workflow look like?
A safe migration follows seven steps:
- Audit every custom module with Upgrade Status and drupal-check
- Decide port, rebuild, or hybrid for each module individually
- Automate the mechanical fixes with Drupal Rector
- Port the remaining logic, dependencies first
- Test against the old module's captured behavior
- Migrate data using the Migrate API with custom source plugins
- Cut over only from a feature-complete staging environment
Sequence matters too. Port the modules other modules depend on first, and leave the site-specific glue code for last. Dependency order flushes out integration problems early, while there's still room in the timeline to handle them.
Treat the migration as the upgrade it is. The module that comes out the other side is testable, dependency-injected, and ready for the next major version instead of dreading it. That's the actual payoff, beyond simply keeping the lights on.
Final thoughts
Custom modules break during migration for one honest reason: they were built for a Drupal that no longer exists. The breakage is predictable, which means it's plannable. Hooks, database calls, variables, theme functions, and custom storage will need attention every single time, so budget for them upfront instead of discovering them mid-sprint.
The teams that struggle are the ones that treat migration as a find-and-replace exercise. The teams that come out ahead audit first, automate what Rector can handle, and spend their human hours on the logic that actually makes their module worth keeping. That last part is the whole game.
And if the audit report on your custom modules looks more like a crime scene than a checklist, that's a normal Tuesday for us. Specbee's Drupal migration team has untangled worse. Talk to us about where your codebase stands.
Frequently Asked Questions
How long does it take to migrate a custom Drupal module?
A small, well-structured module typically takes 2 to 5 days to port, including tests. Complex modules with custom storage, tangled hooks, or undocumented logic can take several weeks each. An Upgrade Status audit gives you a per-module estimate before any code is written, which is why auditing always comes first.
Can Drupal Rector fix my custom module automatically?
Rector rules cover more than 80% of deprecated API uses in the Drupal 12 cycle (Drupal.org, 2026). For upgrades between modern majors, that means most mechanical fixes happen automatically. For Drupal 7 code, Rector helps far less because the changes are architectural. Always review Rector's diff and run tests before committing its output.
Do Drupal 7 custom modules work in Drupal 10 or 11?
No, Drupal 7 modules do not run on modern Drupal without being rewritten. Drupal 8 replaced the core APIs that Drupal 7 modules depend on, including the hook-based menu system, the database layer, and the variables system. Every Drupal 7 custom module needs porting or rebuilding as part of the migration.
Is it cheaper to rebuild a custom module or port it?
Porting is cheaper when the audit shows contained deprecations and the module's logic is still needed. Rebuilding wins when the code has structural problems or a contributed module now covers the feature. Run Upgrade Status first, then check Drupal.org for contrib equivalents before committing to either path.
What happens if I keep running custom modules on Drupal 7?
Drupal 7 reached end of life on January 5, 2025, and no longer receives official security updates (Drupal.org). Extended support programs exist, with some vendors covering Drupal 7 through January 2027, but they're a bridge, not a destination.
Which tools check if my custom code is ready for the next Drupal version?
Three tools cover the audit: Upgrade Status scans custom code against the target version, PHPStan with drupal-check runs static analysis in CI, and Drupal Rector automates the fixes both of them find. All three are free, and the Drupal Association uses the same toolset for its own Project Update Bot.
Priyanka Phukan
Related Insights
Don't Miss Out!