Splitting up your Drupal features thematically

A Drupal project I worked on involved rolling out potentially many (dozens) of sites containing the same starter configuration, content types etc. Essentially we needed a custom install profile that could do a lot of setup for us.

We realised quite quickly that our method had to be strongly Feature-driven, in the Drupal sense. That is, the configuration of most of the site should be managed—initially activated during install, and then maintained in an ongoing fashion—using Features.

However, there was an initial architectural decision we needed to make: how do we split up our configuration into different features?

Features mapping onto, well, features?

Drupal Features (big-F) were originally intended to map reasonably closely to your projects functional features (small-f): that is, you would have a feature for your blog (containing the blogpost content type, and archival listings); a feature for search (containing your search engine configuration, and the configuration of facet-navigation location on the search pages); a feature for Google Analytics (maybe literally just the configuration of your UA-* account number). This was roughly the way we originally configured our offerings at Torchbox, and it works OK.

However, if you have a strong requirement that your features be un-overridden, this presents a difficulty. What if you have a "Document Management" feature, whose requirement is: add "Related Document" media references to certain content types? This feature changes the configuration of every other feature. So where does the Feature for this feature (!) reside?

In the past couple of years, Features Override has been built to solve this specific problem and others like it, but even then it's not guaranteed to cope with every possible circumstance of feature-on-feature (small-f) modifications. Besides, we could choose our starting point completely: why choose one that entailed overriding from the very beginning if we could avoid it? 

One Big Feature?

Much can be said against a One Big Feature approach—literally, all configuration in a single feature—but it does have certain advantages:

  • All your configuration is in one location—much like having all your content in one database—and it's therefore possible to look after, maintain and audit that one location.
  • It's simple, a no-brainer: there's no arguments or ambiguity across a big team as to where a particular piece of configuration should go.
  • If you're undecided about using Features, and the details of how to break them down is part of what's putting you off, this method works.

However, we were in the fortunate position of being able to architect up front (and, as you'll see below, re-architect later), and the dev team was only 2–3 people, so we felt confident we could break features up successfully.

Drupal-driven breakdown of Features

The method we went for in the end was a Drupal-driven breakdown of Features. What do I mean by that? Well, depending on what functional use the configuration had to Drupal (not to the site visitor, or owner) then it would go into different features. This is in some ways the exact opposite of what Features in Drupal 7 was originally intended for (hence its name) and arguably more developer-friendly than consumer-friendly because of it: nobody's going to buy or otherwise acquire a feature called "access control for this site", because if it's for their site, then it's the one they've already got. However, it's suited our developer-driven construction of a Features-driven installation profile.

Our ultimate method is similar in some ways to ones documented elsewhere ("split by conceptual layer", "build in layers") although our breakdown was slightly more architected, including feature-to-feature dependencies, in order to achieve a clean install without any manual intervention.

One late requirement, that prompted our rearchitecting, is that a very small amount of configuration was likely to be (indeed, intended to be) overridden on each deployed site. Because this configuration could be parcelled up separately, this was done so and a new feature created. That means that we are still in a position where 95% of the site configuration can still be maintained as not overridden, and the final feature intentionally ignored (or forked for each site.) It also means we can use features to roll out updates to all configuration except where we expect and indeed desire per-site customization.

Our breakdown using this method

Each feature is named according to this convention:

[client prefix]_[description]

where (a) neither component of the name contains an underscore itself, and (b) "descripttion" is very, very brief: one or two words run together. This both permits us to have a client-specific prefix for all our custom modules, and also to minimize clashes with other prefixes that might contain underscores.

Feature: config (clientprefix_config)

This contained all the filter formats for rich text, and a huge list of system variables. From a user's perspective this would very much be the "misc" feature; for a developer, it made much more sense. 

The only variable settings we didn't include were ones that were specific to e.g. content type display, or Panels layouts: they went in the relevant features below.

Feature: workflow (clientprefix_workflow)

This ended up as the smallest feature, and it just contains a number of Drupal rules. In theory, this feature can in future also contain custom form redirects through alter hooks: at the point of writing, this code was still elsewhere, owing to us having inherited a prototype that was not to be rebuilt in the timescale provided (so not really a prototype!) In future, this feature is likely where new workflow code will go.

Feature: listings (clientprefix_listings)

Almost all Drupal views went here: there are a couple of exceptions that went in "layout structure" as we explain below.

Feature: layout structure (clientprefix_structlayout)

Panels layouts went here. Note that panes in a panel are ID'ed based on (I think) a hash of their entire configuration. That meant that, if any pane was removed, often several pane IDs change, because weights are re-numbered and the hashes change.

Two variables were also included here: default_page_manager_pages and page_manager_node_view_disabled. These actually enable the panels displays, and confusingly they have opposite logic: if both variables aren't present, one or other component still works (I forget which) but you'll be left scratching your head about the other!

Feature: content structure (clientprefix_structcontent)

All the content types, taxonomy vocabularies, field bases and field instances initially went in here. As discussed above, one particular content type was liable to substantial ongoing changes, so we eventually moved that out to the t feature below .

Any system variables pertaining to content types—comments enabling, automatic nodetitling, which menus content can be assigned to—also goes into this feature.

Feature: content structure, customizable content type (clientprefix_structtypename)

Dependencies: content structure.

As mentioned above, we had a single content type that we fully expected to be overridden, in at least one or other field, and possible far more extensively. We therefore decided to put this in its own feature.

We split up the previous feature into two, quite late in the day. Here's roughly how we did it:

  1. Duplicate the content structure feature & rename everything with zmv and perl -i
  2. Edit out everything in the .info file so that (a) the original .info has nothing referring to the customizable content type and (b) the new .info file has all of those entries.
  3. Check in the Features interface that there are no remaining conflicts, and enable the new feature.
  4. Re-export the new feature using drush features-update, and check carefully what has been removed; manually remove the field base for one shared field (see below). This should therefore remove everything not mentioned in the .info file.
  5. Revert and re-export the old feature as above, and again check carefully what has been removed. If you do this before the previous point, you might find all your field configuration being removed from the old feature! This is because it thinks "the new feature's .info file doesn't say so, but when I ask it through API functions, then it returns all of these fields! So I'll just get rid of them."

The one remaining dependency between this and content structure arises because this content type is enabled for use within Organic Groups (OG). That means that it shares the og_group_ref field with all the other content types thus enabled. So the field base goes in the old feature; the field instance—the variation for this one content type—goes in the new feature.

Feature: access (clientprefix_access)

Dependencies: config, content structure, content structure (customizable type).

This feature defines all user roles, permissions and OG permissions for the site. Some of those permissions pertain to specific filter formats (config) or content types (the other two dependencies) and so those features need to be enabled first.

Note that files produced by the OG permissions export tend to fail code review: there are comments far longer than 80 characters in there. As these hardly changed, we fixed them once and then just git-checkout'ed the files every time they were re-exported to fix the comments.

Oh, and the last dependency—on the customizable content type—was such that customization shouldn't affect this feature. If that was going to be the case then we'd probably move those specific permissions out into the custom feature, to isolate the chances of overrides.

Summary

This breakdown is working really well for us. Having to split up the content types at a late stage was a pain: but it was only a couple of hours of pain, followed by an hour or two of testing; we didn't have to trawl every feature, or remove 95% of One Big Feature, to work out what needed to go into the new feature. Now, we install the site, enable all the features, and revert them (that last step seems unavoidable for some reason) and everything else just works from that point on.

However, a slightly different breakdown might work for your own site: if it's views- or rules-heavy, then the listings and workflow features might need to be split up for you. You should feel comfortable experimenting; but I hope you also feel happy that this blogpost has provided you with a good fallback position, should you need it.