Vas ist das?

The Drupal Module Upgrader (DMU) is a module that converts Drupal 7 modules to Drupal 8.

Whoa, dude.

Using DMU

Currently, the only way to run DMU is as a Drush command. Put your D7 module directly in your modules directory -- not a subdirectory -- and enable DMU. Then run drush dmu-upgrade target_module. If there's no output, it means everything worked. If you look at your module now, you should see at least a .info.yml file there -- and, unless you've got a really simple module, a few other things.

For all its awesomeness, DMU isn't omnipotent. There are things it can't effectively convert. For example, if your implementation of hook_menu() contains logic -- an if statement, switch statement, or function call -- DMU will fail with an error, because it cannot effectively trace your module's logic. You'll need to convert it manually.

How didst thou this sorcery?

DMU works by running plugins, each of which is responsible for changing some specific piece of the module's code. Examples include:

Like any other plugin, DMU converters have full access to the Drupal API, and if you run the DMU under Drush (and at the time of this writing, that's the only way to run DMU), you get the Drush API as well. They're implemented using the core plugin system, so you can obviously write your own.

All plugins receive an instance of Drupal\drupalmoduleupgrader\ModuleContext. This object contains the state of the module being converted (i.e., various public properties for plugins to manipulate), plus helpful utility methods for plugins to use.

To analyze, parse, and modify PHP code, DMU relies on Pharborist, which it installs as a dependency via Composer. Pharborist parses PHP code into an abstract syntax tree, which can then be crawled through and modified in a very jQuery-ish way. If you write a conversion plugin that needs to rewrite PHP code in any way (or, for that matter, move functions around), you'll need Pharborist.

Writing DMU plugins

First, you need to decide what kind of plugin to write. There are several types, each of which lives in a particular namespace and may implement a particular interface. All DMU plugins must ultimately implement Drupal\drupalmoduleupgrader\Converter\ConverterInterface, which may itself be extended by some plugin types, in order to define methods specific to that type.

Without further ado, the DMU plugin type buffet:

Module Converters

Plugin\DMU\ModuleWide namespace

Module converters act on the module as a whole, and are generally run only once per module. An example of a module converter is a hook converter -- modules implement hooks only once, so a hook converter should only run once per module. This is the only plugin type that can be specifically enabled or disabled with the --only and --skip options of drush dmu-upgrade.

Route Converters

Plugin\DMU\Routing namespace

These plugins convert individual hook_menu() items to routes. They're also responsible for generating controller classes and moving functions around, as needed. These plugins are identified by the name of the D7 page callback function that they handle -- for example, the drupal_get_form route converter will convert menu items that call drupal_get_form(). Route converters are delegated as needed by the HookMenu plugin.

Function Replacement

Plugin\DMU\FunctionReplacement namespace

These plugins do exactly what you think: rewrite function calls. Like route converters, they're also identified by the name of the Drupal 7 function they handle.

Parametric Rewriters

Plugin\DMU\ParametricRewriter namespace

These are intelligent search-and-replace plugins that act on a complete function. To illustrate, consider this fairly useless function:


function get_uid($account) {
  $uid = $account->uid;
}
    

That first line is problematic in Drupal 8, because to access entity IDs you now use $entity->id(), not direct property access. But just making a grep target isn't going to suffice here, because there is too much we don't know. When DMU looks at this, it has no way to tell exactly what $account is. And for that matter, what if it were named something different, like $user_account? Grep is too dumb to work out these kinds of distinctions, so we need something smarter.

A parametric rewriter is that something. When you use one, you essentially tell it, "Okay -- I'm telling you that the first parameter of this function is a user account. Now rewrite the function as needed." Armed with that information, the rewriter can trawl through the function and make the appropriate changes, depending on the data type. Parametric rewriters are also smart enough to handle getting (as in the above example) and setting ($node->title = 'Foobaz') differently. There are different parametric rewriters to handle different data types; the ID of a parametric rewriter plugin is the type of data it handles, like "node" or "user", although it doesn't have to be an entity type.

Annotation Structure

Plugins are annotated as @Converter blocks, with the following keys:

id
The plugin ID (all plugins in Drupal 8 have one). The ID is meaningful to DMU, depending on what type of plugin you're writing.
description
A short, translated description of what the plugin does.
change_notice
The node ID of a change notice on drupal.org which explains more about the changes the plugin makes. This key is technically optional, but you should strive to include it wherever possible. It can be an array if more than one change notice applies.
message
The default (translated) message to be displayed if the issue handled by the plugin is detected by the dmu-analyze command.
delegator (optional)
Some plugins use delegates, i.e., other plugins which do some work on behalf of the parent plugin. If a plugin needs to use delegates, it can specify the ID of the delegate plugin manager service here, which will be provided to the parent plugin as $this->delegator.
hook (optional)
If the plugin converts a specific hook, specify that hook here, without the hook_ prefix. This only applies to module converters that extend Drupal\drupalmoduleupgrader\Converter\HookConverterBase, and is ignored for other plugin types.

A few words on function replacement...

Not every function replacement needs a full-fledged FunctionReplacement plugin. In many cases, Drupal 7 functions have merely been renamed. If you find yourself needing to convert one of those, breathe a sigh of relief, then keep calm and add it as a target for the Grep plugin :) More about that will be added soon; for now, look at grep.json, in the DMU root directory. You'll figure it out in a jiffy.

So when do you need to write a FunctionReplacement plugin? Easy: when you're rewriting a function call whose arguments, or entire structure, has changed. For example, let us consider the venerable user_access() function, called thusly in Drupal 7:

user_access('break stuff');

In Drupal 8, it looks like this:

\Drupal::currentUser()->hasPermission('break stuff');

It looks intimidating, but this is a simple renaming. Both calls have the same number of arguments, of the same type. The only thing that's changed is everything before the opening parenthesis. So this conversion involves nothing more than adding a new target for the Grep plugin to deal with.

So let's turn up the heat and consider the equally venerable variable_get(). In Drupal 7:

variable_get('pants_type', 'mchammer');

And in Drupal 8:

\Drupal::config('pants.settings')->get('pants_type');

Right there, we see a number of major differences. variable_get() has no idea what pants.settings is, and the get() method doesn't accept a default value for the second argument -- that's expected to be in a YAML file in the module's config/install/pants.settings.yml file. This is a function call that cries out plaintively for a plugin (and it has one; see src/Plugin/DMU/FunctionReplacement/VariableGet.php.