Features Part 3 - Re-usable features

13 comments

In the previous two posts (Features Part 1: A Simple Feature and Features Part 2: Managing Your Feature) I demonstrated how to build a simple feature. The result is something that's particularly useful for two things:

  1. A starting point for new site builds
  2. Dev -> Stage -> Prod style of workflow for site building

If all you want to use features for is the second option, then this is perfectly acceptable. But when you start re-using features across multiple sites, you'll end up needing to fork the feature across each new site build. While you'll no longer have the overhead of creating a blog feature when doing a new site build, you may wish to add new components to your blog feature at some point. How to you push the updated feature across all sites if you've already forked the feature to accommodate necessary customizations?

As a trivial example, assume you have two sites using the same feature, one has 10 blog posts listed per page and the other has 15. i.e. the feature has been forked to allow for the two separate configurations that are needed. Later you decide to add an image field to the blog feature so images can more easily be inserted into blog content. Because the features have been forked, you'll need to manually create the image field (and update the feature) on both sites.

After the site build you will have lost all benefits of using features. This is a shame, because with only a little extra work your feature could be completely re-usable even with the need for different settings on different sites. The idea behind a re-usable feature is to give it Good Defaults TM to begin with so that you don't need to modify it much. But we'll always need to modify some of those defaults.

Prerequisites

This is a more advanced tutorial, and you need to be familiar with the Drupal API to do some of the things described here.

  • You should have read through and have no problem understanding part 1 and part 2 on features.
  • You should be able to write a simple module in Drupal from scratch.
  • Being somewhat familiar with the Drupal API as well as popular contrib modules such as Views will definitely help.

Anatomy of a feature

If you take a look in the module folder of the example_blog feature we built in the previous two posts you'll find a standard module structure (i.e. .module and .info files) and a bunch of .inc files.

example_blog.context.inc			example_blog.info
example_blog.features.field.inc			example_blog.module
example_blog.features.inc			example_blog.strongarm.inc
example_blog.features.taxonomy.inc		example_blog.views_default.inc
example_blog.features.user_permission.inc

Each .inc represents a specific export. You can see above that we have context, field, taxonomy, user_permission, views, and strongarm. Open up example_blog.module and you'll find it's nearly empty, all it does is include example_blog.features.inc which in turn sets up any hooks (ctools, views, cck) that the module needs to use.

Features will never overwrite your .module file, which means you can safely put whatever code you need to in there and not worry about it being overwritten next time you update the feature.

Hooking in

A few of the things we may want to configure in our blog module may include:

  • Home page display
  • Number of posts on the blog listing
  • Block order on the blog page

What we need to do is to basically "extract" those settings from the feature and make the feature configurable itself. In other words, we'll add a second layer of settings to the site.

This can be done either directly in the features .module file or in another .inc file you create and include from the .module. I would personally recommend using the second .inc file. For simplicity, we'll simply edit the .module in this example.

Again, to keep things simple (for me ;-) ) I'm only going to demonstrate configurability of the number of blog posts here. It may make sense in a future post to simply have a bunch of examples for overriding different parts of your site. If there's something you'd be interested in (i.e. how to control block display order, minor setting changes to cck types, etc.) just post a comment on this article and I can add it to that post that may or may not exist at some future date :)

Settings, settings, settings

Open up the example_blog.module file and add a hook_menu(). We need to add an admin settings page for our module so that users can easily make configuration changes.

/**
 * Implements hook_menu().
 */
function example_blog_menu() {
  $items = array();
  $items['admin/config/content/example_blog'] = array(
    'title' => 'Example blog settings',
    'description' => 'Configure the example blog',
    'page callback' => 'drupal_get_form',
    'page arguments' => array('example_blog_admin_form'),
    'access arguments' => array('administer site configuration'),
  );
  return $items;
}

Implement the admin settings form.

function example_blog_admin_form($form_state) {
  $form = array();

  $view = views_get_view('blog_listing');
  $view->set_display('default');

  $default_items_per_page = $view->display_handler->get_option('items_per_page');
  $form['example_blog_items_per_page'] = array(
    '#type' => 'textfield',
    '#title' => t('Items per page'),
    '#description' => t('The number of items to display per page. Enter 0 for no limit'),
    '#default_value' => variable_get('example_blog_items_per_page', $default_items_per_page),
  );

  return system_settings_form($form);
}

Note that in the above example we get our default value directly from the view. Defaults should always be pulled from the feature itself.

The only thing left to do now is to hook into views to set the number of items to display appropriately.

/**
 * Implements hook_views_pre_view().
 *
 * Set the configurable options for views.
 */
function example_blog_views_pre_view(&$view, &$display_id, &$args) {
  if ($view->name == 'blog_listing' && $display_id == 'page_1') {
    $default_items_per_page = $view->display_handler->get_option('items_per_page');
    $view->display_handler->default_display->options['pager']['options']['items_per_page'] = variable_get('example_blog_items_per_page', $default_items_per_page);
  }
}

The key line above here is:

$view->display_handler->default_display->options['pager']['options']['items_per_page'] = ...

This took a bit of time and trial and error for me to figure out. Anytime you need to modify these kinds of objects on the fly you'll just need to get a bit down and dirty with it. I'm not sure if this is the "best" way to handle this, but it works. Each setting you want to export will have it's own unique issues that you'll need to figure out.

Flush your caches and head to admin/config/content/example_blog, set the display items to something else. You should see this successfully change the blog and still not cause the feature to become overridden.

Of course, now you have more settings to export though. Every site I build has it's own feature just for that sites settings. I would add this example_blog_items_per_page variable to strongarm for that feature.

Pitfalls

Anytime you want a feature to be truly re-usable you'll need to make some tough decisions. Primarily: "what should be configurable?" This varies greatly from feature to feature, but try and keep it to just simple settings. If you get complicated you may end up losing a lot of the benefits of features and spending hours and hours on each feature. Just make some decisions and stick to them. Only make settings configurable when you find that they absolutely must be.

In the blog example we used the context module to display blocks in a region of the site. What if you're using different themes that don't have matching regions? Well, then this won't work.

On Wedful we have several themes and ultimately plan to have several dozen. Each of these themes are sub-themed off of a single master theme (which is in itself a sub-theme of zen).

What if you want to distribute your features to clients / end users who definitely don't use the same themes and regions? This is a huge problem, and will continue to be for some time. Fortunately a solution for new sites already exists, and it's called Kit. I won't go into kit here, but Shawn Price has written a post on Kit which I recommend anyone interested in this stuff (which should be ALL Drupal site builders) take a read through.

He's also doing a talk at the upcoming Drupal PWN on this stuff. I wish I could be there for it as I'm sure it'll be full of good stuff.

UPDATE:
It's come to my attention that there's another great post out there on this same topic that Roger Lopez posted just over a year ago with a D6 focus. I'd definitely recommend taking a read through his Building reusable features post as well if you're interested in doing this.

Peanut Gallery

Great article, You mentioned

gmclelland

Great article,

You mentioned the problem about context and using different regions in different themes.
@btopro just made a new module that helps implement global regions that appear in all themes - http://drupal.org/project/regions - See http://elearning.psu.edu/elearning/elms-botany-using-regions-module for a screencast of how this works.

It's also worth noting that using the spaces module you can have a settings page for each feature that only applies to that space.

Anyways, keep up the good work.

Great article. I've used

langworthy

Great article. I've used component override hooks in custom modules but have never thought to make my features configurable :)

I'm also happy to see the Kit shout out. It's an important and seemingly under appreciated project. I see quite a few projects advertising themselves as Kit compliant yet the only project I've found that includes kit tests is Atrium. And when running my own tests on some of these "Kit compliant" projects it becomes clear fairly quickly that they aren't.

I'm really looking forward to seeing this series continue.

https://elearning.psu.edu/ele

gmclelland

https://elearning.psu.edu/elearning/creating-features-ecosystem-through-kit

Shows how you can reuse KIT compliant features on different sites. In their case they are using Atrium Features in the ELMS distribution because they are KIT compliant.

-Regards

You are cool.

Sam

You are cool.

round hole; square peg

Joel C

Thank you for the post. At my job we have a great need for exactly the type of customization you describe. Unfortunately its not hard to see that this paradigm breaks down for anything more than trivial customizations.

For example, for any feature, lets list the number of things that should be trivially configurable in a feature [app?] without reducing yourself to upgrade/configuration hell:

  • displayed titles of the feature [blog -> site announcements]
  • anything in the url [aliases, hooks, menu placements, etc]
  • pagination options, as described above
  • field formatting [max number of characters per field, etc]
  • really, anything related to presentation

Unfortunately, views [and the exporting thereof] in particular was simply not designed for the use case we are describing. Everything from data gathering to presentation to url hooks are bundled into a single component, and unless you want to write a ton of custom code (also a maintenance nightmare), you are doomed to having custom views per feature branch. Even having custom views per feature is not easy as overridding views (by placing the view in a folder that has higher weight than the default) brings its own set of challenges.

I would love to see an alternative to views for drupalers who are more accustomed to the loosely-coupled MVC school of thought. Hopefully the configuration work being done in D8 would allow for some of this.

I don't disagree

Scott Hadfield

I don't disagree with the sentiment here. Unfortunately a lot of the platform building I've done has been round hole, square peg types of problems. In D6 there were times when I needed to completely re-implement (well, copy/paste) core functions into my install profile simply to get around the use of static variables. At least this provides a way to solve most of the problems, granted it all feels a bit hacky.

While I have high hopes for the D8 configuration management stuff, I wouldn't hold my breadth for solving problems like this with contrib modules. Maybe D9 will have better luck, but that's half a decade off at the current release rate :).

I'm not sure enough people

Joel C

I'm not sure enough people have this problem to warrant a generic solution but there are probably several options that if were adopted would make our lives easier:

  • break views exportables up into smaller components, or allow an exported view to be built from code in multiple locations
  • programmatically inspect an overridden view and generate alter statements for the overridden components somehow
  • can't think of any others...

Very cool series of articles

Very cool series of articles Scott. Keep it up! Looking forward to your posts on using Aegir with Wedful.

Great series so far

Awesome articles - been muddling through this stuff for a bit and happy to see I am on the right track - love the idea of basically configurable features - cant wait to try it out. Thanks

Great series so far

Awesome articles - been muddling through this stuff for a bit and happy to see I am on the right track - love the idea of basically configurable features - cant wait to try it out. Thanks

Why not using

Adrian

Why not using hook_views_query_alter() to alter views query but to use hook_views_pre_view() ?

I followed your code but the

Adrian

I followed your code but the value of configure item of page in the admin/config/content/example_blog couldn't be saved.

I did a refresh and it still gave me the default value of 10 from example_blog feature module.

Will it be possible for you

more information

Will it be possible for you to publish an e-book with all these details? It will really help the reference more easier. I could now get a proper grip on the features that are quite useful in the Drupal environment, all thanks to you. These will surely come handy while designing the new websites.

Post new comment

The content of this field is kept private and will not be shown publicly.
  • Allowed HTML tags: <a> <em> <strong> <cite> <code> <ul> <ol> <li> <dl> <dt> <dd>
  • Lines and paragraphs break automatically.

More information about formatting options