Drupal 7 to 8 Module Conversion

Submitted by Kevin on

All blog entries reflect the opinions of the author and have not been expressly endorsed by the Ivan Allen College of Liberal Arts or the Georgia Institute of Technology.

The following was developed using Drupal 8 Beta 15, so a lot of things could still change. The first Release Candidate of Drupal 8 is expected in mid-October 2015, and at that point the API interfaces should be frozen. In the meantime, your mileage may vary, and the author makes no guarantees about anything below actually working in later versions of Drupal 8.

On hearing that the Drupal team is now supporting Drupal 8 beta to beta upgrades, I thought it was time I took a look under the hood and tried to convert at least one of my Drupal 7 modules over to work with Drupal 8.  That was quite an experience, but I'm happy to say I got it working, though the core Drupal coders would probably not be happy with the way I kludged a few things in the end ;-)  Since then, I've converted two more modules, not without a few bumps in the road along the way, but I do have all three working properly now.

The first surprise was that a few betas back they raised the minimum PHP version from 5.4 to 5.5, as it turns out 5.4 is actually going to be EOL later this fall. 

[Insert rant here about how much a pain-in-the-XXXX it is to upgrade PHP on so many common operating systems]

After looking at options for upgrading PHP on my desktop Mac and my RHEL 7 development server and getting a massive headache, I settled for doing my Drupal 8 development on a Ubuntu VM I'd spun up earlier to test PHP 5.6 on.  Drupal installed without any problems and so I got to work figuring things out.

In case you haven't heard this from one of the many other sources of PHP information out there, Drupal 8 has changed a lot from previous versions of Drupal.  Some changes are not too hard to figure out, while others require some in-depth understanding of object oriented PHP programming.

The following drupal.org page is a good starting point for information, but much like with any OO program, the information you'll need from the documentation is actually spread out over a lot of different pages, and finding them isn't all that easy:

https://www.drupal.org/update/modules/7/8

So, in a nutshell, what has changed and what's stayed the same?  Well, they're still called 'modules', but just about everything else is different now.  Before getting into the back end changes, though, there's one big front-end change to be aware of:  modules can no longer be disabled.  When you pull up the module list page (now called 'Extend'), it's not a bug that all of the checked checkboxes are disabled.  There's now only two states for modules: Enabled and Uninstalled.  So, to "turn off" a module, you have to go to the 'Uninstall' tab of the 'Extend' page and uninstall it there.  This basically means that you can no longer turn off a module while leaving its associated data behind.

Directory Structure

Drupal 8 finally moves all of the core components to a directory off of the root named 'core'.  They've also moved what used to be 'sites/all' to be your root directory.  So, the tree looks something like this:

  • webroot
    • core
      • modules
      • themes
    • modules
    • profiles
    • sites
      • default
        • modules
        • themes
        • files
    • themes

So, contrib modules now go in webroot/modules, and core modules are found in webroot/core/modules.

Module Structure

The most notable change to a module's structure is that configuration is done in YAML format now, and hook_menu is history.  So, converting a Drupal 7 module runs along the following lines:

  1. Clone your module into a module directory under your Drupal 8 installation
  2. Rename 'modulename.info' to 'modulename.info.yml' and convert to YAML format
  3. Create a 'modulename.routing.yml' file and convert any hook_menu configuration into its YAML format
  4. Convert your code to work with the routing controller and update anything that uses old API calls that have changed or been removed

I'll take these one-by-one and give a quick explanation of each.

Convert modulename.info to YAML Format

YAML format may look scary at first, but in most cases you can do the conversion with a simple search and replace, changing ' = ' to ': '.  So, these Drupal 7 lines:

name = My Module Name
description = My module's description
package = Foobar

Become this in Drupal 8:

name: My Module Name
description: My module's description
package: Foobar

You do have to add one additional line:

type: module

If you have a 'files = ' section in your old .info file, that gets removed from the Drupal 8 .info.yml file and moved somewhere else.  If you have any other section that takes an array of values, it would be setup like this in YAML:

arrayParamName:
  -  value
  -  value
  -  value

Creating a Routing File

Drupal 8 makes full use of Symfony's routing capabilities, and as such has removed hook_menu and its supporting functions completely.  So, if your module attaches to a URL path, add items to one of the menus, has contextual menu options, or create tabs, you'll have to convert your old hook_menu functions into a modulename.routing.yml file.  The contents for a single URL path attachment look like this:

modulename.modulefunction:
  path: '/your/url/path'
  defaults:
    _controller: 'class path -or- function name'
    _title: 'Menu Item Title'
  requirements:
    _permission: 'access content'

'modulename' is of course the base codename you've selected for you module.  'modulefunction' is a machine readable name for this particular route and should be unique for each route in your module.

Note that unlike in previous versions of Drupal, the leading forward slash is now required on the 'path' value.

The '_controller' value is either a fully qualified path to a class function, or just the name of a regular PHP function.  So, if you're not eager to convert a functional-style module into object-oriented style, you don't have to, though the Drupal folks will tell you it's preferred that you do.

One final note: it appears that the 'requirements' section is required now -- when I left it out, I got an access denied error for my page, even though I was logged in as the root user.  The value of '_permission' is, as in Drupal 7, the name of any defined permission in the system.  'access content' is a good one for all-around access, since even the anonymous user role will have that permission.

If you do choose to go the object-oriented route, there is a preferred organization for your class files within your module.  See the tutorial page here for more information:

https://www.drupal.org/node/2116767

Convert and Update Your Code

To paraphrase one of the documentation pages, at this point you can enable your module in Drupal 8 and start seeing what breaks (probably a lot of things).  This would be a good time to have a working knowledge of debugging techniques, as you're likely to get some white screens of death due to the API changes.  Here's a link to the Drupal 8 API Reference:

https://api.drupal.org/api/drupal/8

Dealing with Custom CSS and Javascript, and Module Output

One thing I've noticed about Drupal 8 is that they seem to have sacrificed simplicity for the sake of more standardization, especially when it comes to further separating the generation of content from the rendering of content in HTML.  The first example of this is that you can't just return a string of HTML from your module anymore.  Now, it has to be embedded in an array (or a special response class, if you're coding OO style), like so:

  return array(
    '#markup' => $outputBuffer,
  );

The upside is that you can add other parameters to the output now, so this would let you override the page title as set in your router definition:

  return array(
    '#title' => t('My Custom Title'),
    '#markup' => $outputBuffer,
  );

Another example of the loss of simplicity is that drupal_add_css() and drupal_add_js() calls to quickly add custom CSS and Javascript no longer exist.  Instead, all CSS and Javascript additions have to be defined as libraries in a modulename.libraries.yml file, like so:

default:
  css:
    theme:
      my_css_file.css: {}

(You can put media descriptors inside the curly brackets to limit the file to only those media types.)

To tell Drupal to use that library, you attach it to the response that your module returns, like so:

  return array(
    '#attached' => array(
      'library' => 'default',
    ),
    '#markup' => $outputBuffer,
  );

Custom Javascript attachment should work the same way, though I haven't had a need to test that out yet.  Beware any docs that say you can add '#css' and '#js' parameters - those were only available in early Drupal 8 betas and have been removed now.  The only way to add CSS and Javascript now is through the libraries method shown above.

Welcome to Plugins

A lot of functionalities that used to be implemented with hooks are now implemented as plugins.  Plugins have to be at least minimally implemented in OO, as they are handled by the class auto-loader system now.  Here are two examples:

  • Blocks - Place proper class files in moduleroot/src/Plugin/Block
  • Input Filters - Place proper class files in moduleroot/src/Plugin/Filter

It looks like Menus and Fields are handled the same way, but I haven't played with them yet to see how they work, and there appears to be support for defining custom plugin types as well.

Take note that the class files that you put into a Plugin folder have to be formatted correctly, or else the autoloader system won't find them (or worse, the catalogger will find them, but the execution part won't, making quite a headache since you don't get any usable error message in this case.)  Check your required 'namespace' line carefully to make sure it is written correctly, and be aware that the autoloader system needs a special @Block comment block in the class file as well.  Take a look at core/modules/shortcut/src/Plugin/Block/ShortcutsBlock.php for a good example of one of these files.

One additional note on blocks:  Blocks in the administrative GUI interface have also changed, as they don't just appear automatically when you enable a module.  Instead, you have to place a block first, which adds a step, but keeps the block organizer a bit cleaner.  The big upside is that you can now place multiple copies of any block, giving you a lot more flexibility in how you use them (and no more awkward kludges to get a block to appear in multiple places - yay!)

And Then There Were Services

Other old functionalities are implemented as services, which require a definition in a modulename.services.yml file in your module's root directory.  One example of a using a service is when you want to override the breadcrumb bar functionality.  To do this, you define a modulename.breadcrumb service, and point it to a class that implements the BreadcrumbBuilderInterface.

A Few Personal Tips

With a system like Drupal, I often find the easiest way to figure out what's going on is to look at a working model.  Of course, there aren't very many contrib Drupal 8 modules yet, but you can dissect the core modules to see how they work.  Pick a simple one, like 'toolbar', rather than one of the big, complex modules, as it will be easier to trace the code in a simple one.  Another useful trick is to Google 'Drupal 8 <function name>' when you know the name of a Drupal 7 API function, but don't know what it's become in Drupal 8.

A word of warning, though.  There's a lot of early beta docs and examples floating around out there (especially on drupal.org) that no longer work in the most recent beta.  So, check the date on any documentation or tutorial before you dig too far into it, as old ones probably won't work any more.

Finally, when you find a useful documentation page on the drupal.org website, bookmark it immediately, as you'll probably never be able to find your way back to it again otherwise ;-)