Features Part 3 - Re-usable features
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:
- A starting point for new site builds
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'] = ...<br>
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.