DMU Plugin Types

Last updated on
30 April 2025

Analyzers

Analyzers scan through target module’s code to determine if any problems -- i.e., things that will not work in Drupal 8 -- exist. If any issues are found, it’s the analyzer’s job to return an object detailing the nature of the problem -- summarizing the issue, pointing out where exactly the problems were found, and referring the developer to documentation on drupal.org explaining how to fix the problem. When you run dmu-analyze, any issues reported by the analyzers are compiled into a purty HTML file which you can peruse at your convenience.

When writing an analyzer, generally you should extend AnalyzerBase. Like most plugin types, every analyzer needs a plugin annotation -- see src/Annotation/Analyzer.php for the list of properties and what they're used for. All analyzers must implement AnalyzerInterface, and reside in the Plugin\DMU\Analyzer namespace.

Here’s a super-simple analyzer, with extra comment sauce:

namespace Drupal\drupalmoduleupgrader\Plugin\DMU\Analyzer;

use Drupal\drupalmoduleupgrader\AnalyzerBase;
use Drupal\drupalmoduleupgrader\Issue;
use Drupal\drupalmoduleupgrader\TargetInterface;

/**
 * @Analyzer(
 *  id = "PSR4",
 *  description = @Translation("Checks if the module defines any classes that need to be moved into a PSR-4 structure."),
 *  documentation = {
 *    {
 *      "url" = "https://www.drupal.org/node/2246699",
 *      "title" = @Translation("PSR-4 compatible class loader in Drupal core")
 *    }
 *  },
 *  message = @Translation("Classes must be PSR-4 compliant.")
 * )
 */
class PSR4 extends AnalyzerBase {
  /**
   * {@inheritdoc}
   */
  public function analyze(TargetInterface $target) {
    // If a module defines one or more classes, then flag a problem. 
    if ($target->getIndexer('class')->count() > 0) {
      // buildIssue() is a method of AnalyzerBase, which creates an issue 
      // using the information in the plugin annotation. You can also build 
      // issues dynamically if you need more control.
      return $this->buildIssue($target);
    }
  }
}

Issues are represented by IssueInterface, which is a glorified property bag containing the issue’s title, summary, documentation links, and relevant locations (file name and line number) in the target module’s code.

An analyzer can return a few different things:

  • A single IssueInterface if you want to flag a single issue.
  • An array of IssueInterface objects, if you want to flag several issues.
  • NULL if no issues were detected.

It’s technically possible for an analyzer to modify the code it’s analyzing, but this is verboten! Analyzers are meant to be read-only. Look closely, but do not touch. (If you use an analyzer to alter code, I will lose all respect for you and punch you.)

Converters

Converters are DMU’s meat-and-potatoes. They take the original Drupal 7 code and do what they can to rewrite it for Drupal 8.

When a converter encounters something it can’t convert, it may choose to leave a comment at the affected area with a FIXME notice, informing the developer what needs to be fixed. If the converter detects that the original code will break in Drupal 8, it can also comment out the affected lines.

Code generated by converters doesn’t have to be beautiful, or even necessarily follow best practices. DMU was never intended to be a one-stop module upgrader, and it’s not its job to refactor your module for you. (In fact, ugly code generated by converters should be an incentive for developers to refactor! :)

Converters modify the target module in-place, so it pays to make a backup beforehand (the dmu-upgrade command has a --backup option which does this). The ultimate goal of the converters is to make the target module enable-able in Drupal 8 without blowing the site up.

Like analyzers, converters have a plugin annotation, which is documented at src/Annotation/Converter.php. They also have an interface they must implement (ConverterInterface), and a helpful base class (ConverterBase) which you can extend when writing your own.

Here’s one of the simplest converters included in DMU:

namespace Drupal\drupalmoduleupgrader\Plugin\DMU\Converter;

use Drupal\drupalmoduleupgrader\ConverterBase;
use Drupal\drupalmoduleupgrader\TargetInterface;

/**
 * @Converter(
 *  id = "hook_permission",
 *  description = @Translation("Converts static implementations of hook_permission() to YAML."),
 *  hook = "permission"
 * )
 */
class HookPermission extends ConverterBase {
  /**
   * {@inheritdoc}
   */
  public function convert(TargetInterface $target) {
    // Check module for implementation of hook_permission().
    $indexer = $target->getIndexer('hook');
    if ($indexer->has('permission')) {
      // If found, evaluate the hook so that it will exist.
      $hook = $indexer->get('permission')->get(0);
      eval($hook->getText());
      // Execute the hook, and write the returned array to MODULE.permissions.yml.
      $this->writeInfo($target, 'permissions', call_user_func($hook->getName()->getText()));
    }
  }
}

Cleaners

Cleaners are the janitors of the DMUniverse. As their name implies, they clean up an already-been-ported module by removing legacy code, defunct hooks, and other such detritus. When you use the dmu-upgrade command, cleaners will be run if you specify the --clean option (note: at the time of this writing, this option is broken due to what appears to be a problem with Drush). Cleaners are responsible for deleting all the cruft that has no use in Drupal 8. This can take many forms, but the most basic example is deleting implementations of hooks that no longer exist.

To “delete” a piece of code, you simply get ahold of its Pharborist node, call the remove() method, and...that’s it, time for a drink. The node will be kicked out of its syntax tree, and once it’s saved back to disk, the file will reflect the change.

Cleaners implement CleanerInterface and go in the Plugin\DMU\Cleaner namespace. Here’s the simplest cleaner imaginable:

namespace Drupal\drupalmoduleupgrader\Plugin\DMU\Cleaner;

use Drupal\Core\Plugin\PluginBase;
use Drupal\drupalmoduleupgrader\CleanerInterface;
use Drupal\drupalmoduleupgrader\TargetInterface;

/**
 * @Cleaner(
 *  id = "info",
 *  description = @Translation("Deletes the target module's Drupal 7 .info file.")
 * )
 */
class InfoToYAML extends PluginBase implements CleanerInterface {

  /**
   * {@inheritdoc}
   */
  public function clean(TargetInterface $target) {
    // Delete the old module.info file. So long, sucker.
    unlink($target->getPath('.info'));
  }

}

Indexers

As you might imagine, Pharborist syntax trees can consume a fair chunk of memory. Because of that, it’s not realistic to hold the code of a target module entirely in memory while analyzers, converters, and cleaners work on it -- you slam into your memory_limit real fast.

To get around that, DMU creates an index of the target module which can be queried to determine whether the module implements certain code and, if so, where that implementation exists. Indexers are plugins, and each one is basically a glorified wrapper around a database table.

When you run the dmu-analyze, dmu-upgrade, or dmu-clean commands on a module, the first thing that happens is that the module is indexed. Every available indexer plugin is created and put to work on the target module. They’re responsible for cataloguing things like:

  • What hooks a module implements, and where those implementations reside (i.e., in which files)
  • Classes defined by the module
  • Functions defined by the module
  • Tests defined by the module, and what types they are
  • Which functions are called by the module, and when those calls occur

It’s up to individual indexers to decide what’s worthy of being indexed, but any and all information gathered by an indexer will be available for other plugin types to use. Indexers must implement IndexerInterface and reside in the Plugin\DMU\Indexer namespace.

Indexers are fairly low-level and generally, you won’t need to write your own. The only time you’d want to create one is if you needed to deal with things that have their own overarching architectural concepts -- CTools plugins, for example, or Views handlers.

Parametric Rewriters

Parametric rewriters have a weird, space-agey name, but they’re not as intimidating as they sound. Parametric rewriters are a DMU plugin type, but they’re essentially “utility” plugins. Internally, DMU refers to them simply as “rewriters” for short.

A parametric rewrite is, in essence, a smart search-and-replace wherein the searching and replacing is based on a data type known ahead of time.

If your reaction to that was “wat”, that’s okay. Consider this function:

function foo_node_insert($node) {
  echo $node->nid . ‘ has been inserted.’;
}

I pose to you this monumental question: what exactly is $node?

Easy, you say -- it’s a node object (stdClass if you want to get technical). But can you always absolutely count on that? There’s no type hint or anything. Do you know it’s a node just because it’s named $node? Because the function is an implementation of hook_node_insert()? These don't guarantee anything at runtime. From a technical standpoint, there’s no particular guard against $node being, say, a user account, or even a scalar (which would at least result in a fatal error). Some ill-behaved code somewhere could easily pass any old garbage into this function. Therefore, from the perspective of a DMU plugin, there’s no real way of knowing precisely what $node is, or what $node->nid is supposed to be.

This is the problem that parametric rewriters are intended to solve. When you use a parametric rewriter, you hand it a function parameter (that is, a \Pharborist\Functions\ParameterNode object) and, in effect, explicitly tell the rewriter: “hey, I expect this here parameter to be a node/user/taxonomy term/magic artifact/whatever”. Armed with this knowledge, the parametric rewriter can know that $node->nid is the node’s ID property, which in Drupal 8 has been replaced by the $node->id() accessor. The parametric rewriter uses the name of the parameter, so if $node was called $blorf, it’d simply look for $blorf->nid instead of $node->nid.

Like I said: search-and-replace, but smarter. After being run through a parametric rewriter, foo_node_insert() would look like this:

// Parametric rewriters will also adjust the parameter's type hint if need be.
function foo_node_insert(\Drupal\node\NodeInterface $node) {
  echo $node->id() . ‘ has been inserted.’;
}

There is a parametric rewriter for each type of data that can be handled. There’s one for nodes, one for users, one for taxonomy terms, and so forth. You can, naturally, create your own parametric rewriters if you need to handle other data types. In their plugin definition, parametric rewriters list the properties that they know how to handle, like so:

@Rewriter(
	id = “node”,
	type_hint = “\Drupal\node\NodeInterface”,
	properties = {
		“nid” = {
			“get” = “id”
		},
		“title” = {
			“get” = “getTitle”,
			“set” = “setTitle”
		}
	}
)

A parametric rewriter with a plugin definition like this will know to replace $node->nid with $node->id(), and $node->title with $node->getTitle() or $node->setTitle(), depending on whether the title is being get or set. Yes -- a parametric rewriter is smart enough to determine if a property is being get or set. How?

Well, when you’re setting a property (or a variable, or an array element, or anything), it always looks like $x = $y. The destination is always on the left side of an equals sign. It’s a cinch to parse an assignment like this in Pharborist. Anything that’s not an assignment, then, is a getter.

So, when a parametric rewriter runs, it first scans the parameter’s function for expressions like $parameter->some_property or $parameter[‘some_property’]. It loops through each matching expression to see if the property name can be determined — for example, $node->$id_field could be almost anything, and it’s impossible to determine what $id_field will be. So expressions like that are skipped.

Once the property is determined, the rewriter checks if the expression is the left side of an assignment in order to determine whether it should be a getter or a setter. If the property being get (or set) is not defined in the plugin definition, the expression is simply left as-is. If a property is defined without a setter, getter expressions will be rewritten but setter expressions will be left alone. The same principle applies to setters without getters.

Parametric rewriters are a powerful concept. You can use them to convert stdClass property bags to classed node objects, mapping public properties to getters and setters. And they work for arrays too — there is a parametric rewriter which can convert a $form_state array to the various getters and setters of FormStateInterface.

All this being said, chances are that you won’t need to write your own parametric rewriters — the bundled ones should suffice for most uses. But if you need to write one, it should implement RewriterInterface and live in the Plugin\DMU\Rewriter namespace.

Help improve this page

Page status: Not set

You can: