When you’re converting Drupal 7 code to Drupal 8, there are certain things that you have to do fairly often.

Simple Find and Replace

A large number of Drupal 7 “things” (global variables, functions, and constants) were moved into service objects in Drupal 8. Otherwise, they were pretty much left alone -- apart from having a different name, they have the same number of arguments, of the same type. In cases like these, all you really need is a simple find-and-replace.

The good news is that we wrote a plugin for that: it’s called Grep, and its behavior is controlled entirely by a configuration file. To add a new search/replace target to Grep’s configuration, you need only save it in the appropriate section of the drupalmoduleupgrader.grep configuration object. (See DMU's config/install/drupalmoduleupgrader.grep.yml file for more information on how this works.)

This is a pretty nice solution for simple, dingbat search-and-replace, but there are times when that won’t cut the mustard. You may find yourself in need of something more complex. When that happens, you need to write a converter.

Function Call Modifiers

Consider this humble snippet:

variable_get(‘pants_type’, ‘mchammer’);

What does it look like in Drupal 8?

\Drupal::config(‘pants.settings’)->get(‘mchammer’);

Clearly, a search-and-replace is not powerful enough to handle this. What we need is a function call modifier.

A function call modifier is a specialized type of converter. As the name implies, it is handed a single FunctionCallNode to play with, rather than a full-fledged target module. To implement a function call modifier, you extend Plugin\DMU\Converter\Functions\FunctionCallModifier, and implement the rewrite() method. Exactly how the function call will change depends on what this method returns.

The rewrite() method has three possible return values:

  • If you return the original FunctionCallNode (as determined by object identity, meaning the === operator), nothing will happen. The call will stay right where it is, nestled snugly in the syntax tree, and any modifications you have applied to it will be saved. This is a good way to modify a function call in-place -- for instance, changing the function’s name, or removing an extraneous argument.
  • If you return a different node (of any type) -- again, determined by object identity -- it will replace the old function call. It’ll just be swapped in, and the old call will be thrown out.
  • If you return NULL (or some other empty value), the conversion will assume that the function call could not be rewritten, so it will comment out the containing statement and slap a FIXME comment (pulled from the plugin definition’s fixme property) above the offending call.

Here’s an example of a simple function call modifier. This one works on the defunct drupal_map_assoc() function, replacing it with a call to array_combine():

use Drupal\drupalmoduleupgrader\TargetInterface;
use Pharborist\Functions\FunctionCallNode;

/**
 * @Converter(
 *  id = "drupal_map_assoc",
 *  description = @Translation("Rewrites calls to drupal_map_assoc().")
 * )
 */
class DrupalMapAssoc extends FunctionCallModifier {

  /**
   * {@inheritdoc}
   */
  public function rewrite(FunctionCallNode $call, TargetInterface $target) {
    // Change function name to array_combine().
    $call->setName('array_combine');

    // Duplicate the first $array argument twice (silly, but true).
    // Need to clone the argument to make a copy of it, since Pharborist works
    // on original tree elements.
    $arguments = $call->getArguments();
    // Most Pharborist manipulation methods, including appendArgument(),
    // return $this for chaining.
    return $call->appendArgument(clone $arguments[0]);
  }

}

ArrayPIs to YAML

Ah, the venerable ArrayPI: known and loved by Drupal developers the world over, these are those “info” hooks, like hook_menu, that return big, unwieldy arrays of information. A fair number of these have been removed from Drupal 8 and replaced by static YAML files.

To convert one of these to YAML, the easiest way is probably to just get ahold of the hook implementation, eval it into existence, execute it, and then pass the returned array directly to Symfony’s YAML dumper.

I can hear your chest-clutching gasp of horror now: eval it?!? You heard me.

You could probably traverse the array with Pharborist, but that would be time-consuming, laborious, and prone to error. Assuming the hook implementation doesn’t contain any logic -- if statements, branching operators, function calls, object instantiation -- anything, essentially, that might influence the returned array -- why not?

DMU provides a filter to allow you to quickly test a function (or any node) for the presence of “logic”, like so:

use Drupal\drupalmoduleupgrader\Utility\Filter\ContainsLogicFilter;

/**
Assume we're working on these dumb functions:

function foo() {
    return array(‘bar’ => ‘baz’);
}

function baz() {
    return array(‘foo’ => _bar_get_info());
**/

// I also call this the Vulcan filter. Yuk yuk.
$is_logical = new ContainsLogicFilter();
// $foo and $baz are FunctionDeclarationNodes.
$foo->is($is_logical);    // Will return FALSE
$baz->is($is_logical);    // Will return TRUE because of _bar_get_info() call

I dunno about you, but I’d say foo() is pretty safe to evaluate and execute.

At the time of this writing, ContainsLogicFilter will return TRUE if it finds any of the following things anywhere in the tested node:

  • if statements
  • switch statements
  • class method calls, like \Drupal::database()
  • object instantiation, like new EntityController()
  • function calls (although ContainsLogicFilter does allow you to "whitelist" certain functions which should pass the filter)

If you’re still reading this (instead of, say, barfing on your shoes at the thought of eval’ing non-logical Drupal 7 hooks), you can probably already see a simple way to convert an ArrayPI to YAML:

use Symfony\Yaml\Yaml;

$simple_hook = $module->find(\Pharborist\Filter::isFunction(‘foo_permission’))->get(0);
eval($simple_hook->getText());
$hook_result = call_user_func($simple_hook->getName()->getText());
Yaml::dump($hook_result, ‘path/to/foo.permissions.yml’);

And in fact, this is exactly the approach taken by hook_permission converter, as well as a few others.

(P.S. If you’re still green in the face over the use of eval, there is work going on in Pharborist to make array traversal easier. For now, we can only imagine an eval-free world, but we may get there yet.)