Architecting a Drupal 7 module into multiple files

In the Drupal modules that I inherit or review, I see a lot of different ways of factoring out into separate files, of what might have begun in the main module file. This can be useful for performance (to a limited extent) and legibility, but depending on how you do it, you might end up ironically spoiling both.

How should you break down your Drupal module files? Well, I'm not here to tell you the perfect file breakdown. Matching the architecture is good, although what "the architecture" means in Drupal 7 isn't clear. Outside of a Drupal 8/Symfony-style architectural model, there's a limit to how much the file breakdown really needs to match the architecture, and a limit to how useful doing so would be.

Unless you're quite experienced, and have good reasons for doing so, you should avoid making up your own scheme if you can: by default, just have your module in one file. But if you're determined, then even Drupal 7 provides you with a number of core and contributed mechanisms for factoring code over multiple files.

For beginners in Drupal module development, therefore, below is a summary of some of the standardized ways in which Drupal lets you break your module code into different files.

What to consider before you start

Avoid PHP's own include or require functions

Before we start talking about how to break down files, we should briefly discuss how you bring them back together again at runtime in Drupal. To do so, you should avoid using PHP's include* or require* statements in your code, unless you really know why you're using them.

In their place, you can almost always use Drupal's module_load_include(). This encourages you to follow standard naming conventions etc, and so, as you use it more and more, your code should become clearer and more compact.

Avoid always including files you've factored out

You should also avoid automatically including any files at the top of .module, outside of all functions. From Drupal's perspective (and a performance perspective) this is equivalent to just having them inline; also, Drupal expects file including to have no side effects, which means all code should be in classes and/or functions only.

If you've ended up in this situation because you're splitting .module for legibility reasons, then see the suggestions below instead.

Avoid multiplying files solely for legibility, before you've considered other options

Finally, if you're considering breaking things down because you want to make .module more legible, you should do two things instead:

  1. Follow all Drupal coding standards (and automate checking of those standards). This will improve your module's legibility no end.
  2. Read the rest of this blogpost.

Once you've done these two things, you're unlikely to ever need to begin .module with lots of include statements, but can instead focus on breaking down your files in standard ways, as discussed below.

Code that should only go in certain files

All (core) hooks in the .module file

All core hooks must go in .module, because that's how core knows they exist. Most third-party modules which define their own hooks also expect them to be there.

(If your core hook code ends up long and messy, and you really want to factor it out, see towards the end where I talk about stubbing out.)

Other hooks in separate files, depending on if third parties have done the right thing

You can always leave all your hooks in .module, of course (because you can leave all your code there), but some contributed modules (e.g. media, pathauto, features) implement hook_hook_info(). This hook assigns their custom, declared hooks to groups: you can then put your module's definition of those hooks into e.g. .groupname.inc, and they'll get automatically loaded.

Views does something similar, permitting hooks to go into a .views.inc, but it doesn't seem to implement hook_hook_info() to do it. Also, for some reason, no modules in Drupal core implement this hook; which is a bit of a shame, but there you go.

Classes in locations declared in your .info file

In .info you can add entries beginning files[] = relative/to/moduledir, which can reference files where classes are stored. Drupal interrogates these files and builds a registry of class locations, enabling class autoloading so you don't need to always worry that your class is in scope before using it.

When you add an entry to your .info file, you should clear caches. Avoid having more than one class per file (and name your file after your class, in camelcase); but if you do add another class to an existing file, you should clear caches or ideally rebuild the autoload registry.

Callbacks you define, that can go in other files

A number of Drupal hooks, core and otherwise, permit you to specify one or many files that needs to be loaded in order for a callback to then be invoked. You should use this as an opportunity to factor out callback code sooner rather than later.

Your module's page callbacks can go in separate files

When you define a page callback with hook_menu(), you can define a file and filepath for each route:

<?php
/**
 * Implements hook_menu().
 */
function mymodule_menu() {
  $items['admin/config/system/my-config'] = array(
    'title' => 'My configuration',
 
    'page callback' => 'drupal_get_form',
    'page arguments' => array('mymodule_admin_config'),
    'access arguments' => array('administer mymodule'),
 
    // All admin form callbacks in .admin.inc.
    'file' => 'mymodule.admin.inc',
  );
  return $items;
}

How you architect all your callback code is partly up to you. However, you might want to consider:

  • A .pages.inc, with all functions beginning mymodule_pages_*(), for public-facing page callbacks.
  • A .admin.inc, with all functions beginning mymodule_admin_*(), for configuration page callbacks.

Ensure that, however you break it down, it's consistent, documented, and clearly laid out (with comments) in the hook.

Your theme templates and preprocess hooks can go in separate files

When you declare a new theme hook, your module's templates can go in one folder, and preprocess hooks can go in one or many other files. You might want to consider only having a single file for all your preprocess hooks and function-based theme definitions, .theme.inc, and having all your theme files in a theme/ subfolder:

<?php
/**
 * Implements hook_theme().
 */
function mymodule_theme() {
  $theme_subfolder = drupal_get_path('module', 'mymodule') . "/theme";
  $items['mymodule_mytheme'] = array(
      'path' => $theme_subfolder,
      'template' => 'mymodule-mytheme',
 
      // All preprocess hooks in .theme.inc.
      'file' => 'mymodule.theme.inc',
    ),
  );
  return $items;
}

The above code both implicitly defines a template mymodule-mytheme.tpl.php, and explicitly defines a location for preprocess hooks mymodule.theme.inc, all in a subfolder. Again, comment your architectural decision so it's clear what you've done.

Other callbacks

Lots of third-party modules provide inner platforms with their own architecture, hooks, plugins and includes: I don't cover them here, though, as this is intended as a guide for beginners. Where possible, though, consider taking advantage of such an architecture sooner rather than later: it'll be more awkward to refactor everything into separate files later; you might also then get warnings of missing functionality (especially classes) until you clear caches or rebuild the registry.

Arbitrary breaking-down of your remaining code

Finally, here are a couple of tips for when you want to construct your own file hierarchy even further. I won't dictate to you what that hierarchy should be, but instead I provide a couple of tricks to get you moving in the right direction.

Commonly-used code in an API file

If your module has some central functionality—for example, integrating with an external API, or making complicated calculations—that's used in several different scenarios—for example, in a core hook, but also part-way through a template preprocess callback—then it makes sense to store that in a separate file, which can be brought in only when absolutely necessary.

You can call your shared file .api.inc or even simply .inc:

<?php
/**
 * Implements hook_init().
 */
function mymodule_init() {
  if (user_access("use mymodule remote services")) {
    // Loads mymodule.api.inc.
    module_load_include("api.inc", "mymodule");
    // Shorter alternative: loads mymodule.inc.
    module_load_include("inc", "mymodule");
 
    mymodule_api_some_remote_service();
  }
}

Ideally you should include this shared file every time you use one of its functions; in practice, if the function you're currently writing is internal to your module (its name begins with an underscore) and you know it's only ever called after the API file is included, you can probably wing it.

Stubbing out large hooks into a separate file

If you have large—and I mean really large—hooks in .module, the meat of which is generally not necessary; and if you genuinely believe they're harming legibility; you can move them out into a file of your choosing:

<?php
/**
 * Implements hook_init().
 */
function mymodule_init() {
  if (user_access("administer site config")) {
    // Call separate stub for performance/readability reasons.
    module_load_include("stub.inc", "mymodule");
    _mymodule_stub_init();
  }
}

But now you're creating not just your own module and code, but also your own architecture. This can also make your module harder to maintain, because you're now making up your own architecture, and there's no guarantee any other Drupal developer will think the way you do.

So if you decide to do this, ensure your code is up to Drupal standards (you're already doing that, right? because we talked about that earlier, right?) and comment, comment, comment; and document, document, document. Your future self and the rest of your (future) team will thank you for it. I'm as guilty as anyone else of underdocumenting the architectural decisions I make, so we can all of us always do more, and do better.

Summary

Drupal 7 and its ecosystem of contributed modules offer a number of standard ways to separate code into different files in your module's folder. You should feel very comfortable using the standard ways, as long as your code is up to Drupal's coding standards, and as long as you comment your decisions; on the other hand, you should feel uncomfortable making up your own architecture, unless you strive to be consistent, and unless you make crystal-clear every architectural decision you've made.

Comments

Hi there J-P, thanks for sharing your insight into the crazy world of Drupal code architecture ;-) I am currently building out an install file on a client's module and getting that uncomfortable feeling you have written about. The install file is taking care of setting up and populating a load of content types (with associated fields), taxonomies, menus and views. I have split the file up using module_load_include and sub-files (my-module.node.inc, my-module.fields.inc, my-module.taxonomy.inc, etc.). This has made the code more manageable but probably at the risk of sane architecture.

Ever had to deal with this or any advice on how you would tackle it?

Thanks, Mike.

Install files only get pulled into Drupal's active code on very rare occasions, so unless you've a specific architectural reason for splitting them into other files (e.g. you're going to need some, but not all, of your code in some other code elsewhere) then I simply wouldn't do it. All in one file, but make sure you follow Drupal coding standards and comment functions properly with docblocks.

What you describe shouldn't involve very much code anyway. Any reason for not using features? This is the standard way to create content types, views, taxonomy vocabularies etc. and it handles its own file-by-file separation. It would make most of the separate files you mention go away, I think.

(Incidentally, I've got suggestions for how to split up your features too, to avoid fragmenting features too much.)

Thank  you very much for this, J-P.

I'm wondering what you think about enfity definition.  It is possible, and I think perhaps common, to put any entities that you define in a separate module, but that is not really necessary.  You could put the schema definitions in your module's .install file and put the rest of the entity definition in a separate .inc file, but that separates code that really ought to be together, don't you think?

I'm not totally sure what you mean by "entity definitions", but regardless, I would use Features:

  • If you mean actual entities (e.g. nodes) then you can use the UUID module in conjunction with Features. However, I've generally found UUID to be a bit awkward to work with (the Features rapidly become overridden, even with no edits.)
  • If you mean bundles (e.g. content types) then again, export using Features. Non-node bundles can be defined and exported with the help of ECK: even if you don't specifically use ECK to build the bundle, you should export into an ECK-compatible file breakdown and function naming.

Either way, you're still breaking them down into files: you're just letting Features define the standard for how that happens.

Thanks again, JP, and I'm sorry if I wasn't clear.

By "Entity" I meant my own defined entity, using hook_entity_info, hook_entity_property_info, and my own class overrides and CRUD functions.

As it happens, I got a reply on Drupal Answers to a different version of this question.  acrosman says that I am correct, and that it IS common practice to put the entity definition in its own module and then include that module as a dependency in any module that uses the entity.  If I followed this approach, I would have to figure out a way to package the two modules together in a distribution, but that is, for me, a problem for another day.

Wyckham

Yeah, ECK features export supports entity types too: but you can implement how you want! Feel free to link to the Drupal Answers post here, though, so people have options....

 

JP,

I tried to post the link, but Mollom's spam flter blocked  me for some reason.

Oh! I'm afraid Mollom's a closed book to me, sorry.

JP,

Thanks very much for this post. Really excellent article!

 

Add new comment