The Drupal Module Upgrader (DMU) is a module that converts Drupal 7 modules to Drupal 8.
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.
DMU works by running plugins, each of which is responsible for changing some specific piece of the module's code. Examples include:
src/Plugin/DMU/Module/InfoToYAML.php
).hook_menu()
-- the ones that don't have any special logic -- to various YAML files, converting the
callback functions into methods in a controller class
(src/Plugin/DMU/Module/HookMenu.php
).hook_init()
and hook_exit()
to callback methods in event subscriber classes
(src/Plugin/DMU/Module/HookInit.php
and src/Plugin/DMU/Module/HookExit.php
).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.
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:
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
.
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.
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.
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.
Plugins are annotated as @Converter
blocks, with the following keys:
dmu-analyze
command.
$this->delegator
.
Drupal\drupalmoduleupgrader\Converter\HookConverterBase
, and
is ignored for other plugin types.
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
.