Exporting your module configuration using Ctools or with custom code - when to use which method?
When I recently joined Scott Reynen as maintainer of the @font-your-face module (blog post), one of the first tasks on my list was to enable site builders to save their font settings using Features. For those not known with Features; it’s a module that saves database settings (views, content types, variables etc) to code. This enables you to save (various) versions of your site in VCS and then move settings from development/staging/production without ‘clicking’. Features is pluggable: if a module has its own hooks for Features, it settings may be exported as well. A lot of contrib modules already come with Features implementation (Views, Rules, Context, Boxes, etc) and we thought it’d be great if we could add @font-your-face to this list as well.
The Features integration of the @font-your-face module
There are basically two options to make your module exportable: using Ctools or by writing your custom code. Using Ctools is by far the easiest and simplest method to implement, but it comes with some limitations. Using custom code gives you more flexibility, but it involves also a bit more effort.
Exporting configuration using Ctools
Ctools (chaos tools suite) is an API module used by more and more contrib modules. Panels, Views, Rules UI, Boxes, Context; all of them rely on Ctools. It comes with an export API that you can use to export your own module configuration in a standardized way. It involves three simple steps:
- Define the data you want to export by expanding your hook_schema() with an ‘export’ part.
- Write a load() function
- Write a save() function
Stella wrote a great blog post about this method and more details can be found on Drupal.org: http://drupal.org/node/928026
Basically, Ctools export moves your database settings to code and once they are exported, Drupal doesn’t use the database entries anymore. This method works great in most use cases. And the good thing is that it comes with an admin interface where you can enable/disable/revert your custom settings, similar to the interface that comes with Context and Views. But I found two drawbacks that made that I couldn’t use this method for the @font-your-face module:
1. The ‘Create Feature’ interface lists ALL settings, even those that are not enabled
This works fine if you have ten content types, or twenty views. But with @font-your-face you can import several thousands of fonts and then enable just a few of them. As Features shows all configurations independent of their state, the lists of Components became extremely long and slow to use. I couldn’t find a way to filter this list of checkboxes using the default Ctools Export UI.
2. The exported configurations cannot be retrieved using Views
The function that loads all your objects (ctools_export_crud_load_all()
) first retrieves all the stored settings from your code and then combines this with the database settings for all settings that have not been exported. The @font-your-face admin interface uses a nifty Views interface to simplify the admin UI. It shows all the imported fonts (which can be a several thousand) in a nice interface with filters, sorts, etc. By exporting fonts using Ctools they were moved out of the database and thus cannot be retrieved by Views anymore.
I’ve posted an issue in the Ctools issue queue to highlight my findings and merlinofchaos confirmed this behaviour. As Scott and I didn’t want to give up the Views admin interface, we needed another method.
Exporting configuration using custom code
It is also possible to write your own hooks for features. By doing so, you can define which configrations to show in the export interface, and how they are loaded. I’ll show the code that I’ve used for the @font-your-face module as this explains better how it works. The steps needed to get this working are:
1. Make your module known to Features using hook_features_api()
Implement hook_features_api() in your .module file (fontyourface.module).
/**
* Implements hook_features_api().
*/
function fontyourface_features_api() {
return array(
'fontyourface' => array(
'name' => '@font-your-face',
'file' => drupal_get_path('module', 'fontyourface') . '/fontyourface.features.inc',
'default_hook' => 'fontyourface_features_default_font',
'feature_source' => TRUE,
),
);
}
2. Define which settings you want to list in the export UI using hook_features_export_options()
This hook will alert features of which specific items of this component may be exported. For instances, in this case, we want to make available all the existing items. If there are no items to be exported, this component will not be made available in the features export page.
/**
* @return array
* A keyed array of items, suitable for use with a FormAPI select or
* checkboxes element.
*/
function fontyourface_features_export_options() {
$fonts = array();
foreach (fontyourface_get_fonts('enabled = 1') as $font) {
$fonts[$font->name] = $font->name;
}
return $fonts;
}
3. Use hook_features_export() to add your dependencies to the FEATURENAME.info file
This is a component hook, rather then a module hook, therefore this is the callback from hook_features_api which relates to the specific component we are looking to export. When a specific instance of the component we are looking to export is selected, this will include the necessary item, plus any dependencies into our export array.
/**
* @param array $data
* this is the machine name for the component in question
* @param array &$export
* array of all components to be exported
* @param string $module_name
* The name of the feature module to be generated.
* @return array
* The pipe array of further processors that should be called
*/
function fontyourface_features_export($data, &$export, $module_name = '') {
// fontyourface_default_fonts integration is provided by Features.
$export['dependencies']['features'] = 'features';
$export['dependencies']['fontyourface'] = 'fontyourface';
// Add dependencies for each font.
$fonts = fontyourface_get_fonts('enabled = 1');
foreach ($fonts as $font) {
if (in_array($font->name, $data)) {
// Make the font provider required
$export['dependencies'][$font->provider] = $font->provider;
$export['features']['fontyourface'][$font->name] = $font->name;
}
}
return $export;
}
4. hook_features_export_render() renders the actual settings to export
This hook will be invoked in order to export Component hook. The hook should be implemented using the name ot the component, not the module, eg. [component]_features_export() rather than [module]_features_export().
/**
* Render one or more component objects to code.
*
* @param string $module_name
* The name of the feature module to be exported.
* @param array $data
* An array of machine name identifiers for the objects to be rendered.
* @param array $export
* The full export array of the current feature being exported. This is only
* passed when hook_features_export_render() is invoked for an actual feature
* update or recreate, not during state checks or other operations.
* @return array
* An associative array of rendered PHP code where the key is the name of the
* hook that should wrap the PHP code. The hook should not include the name
* of the module, e.g. the key for `hook_example` should simply be `example`.
*/
function fontyourface_features_export_render($module, $data) {
$fonts = fontyourface_get_fonts('enabled = 1');
$code = array();
foreach ($data as $name) {
foreach ($fonts as $font) {
if ($font->name == $name) {
unset($font->fid); // unset the identifier, as this may not be the same on other environments (staging/production/etc)
$code[$name] = $font;
}
}
}
$code = " return " . features_var_export($code, ' ') . ";";
return array('fontyourface_features_default_font' => $code);
}
5. And finally, make sure your module loads it settings from the exported code once a feature gets reverted.
/**
* Implements hook_features_revert().
*/
function fontyourface_features_revert($module) {
fontyourface_features_rebuild($module);
}
/**
* Implements hook_features_rebuild().
*
* Rebuilds @font-your-face fonts from code defaults.
*/
function fontyourface_features_rebuild($module) {
$saved_fonts = module_invoke($module, 'fontyourface_features_default_font');
foreach ($saved_fonts as $key => $font) {
$font = (object) $font;
$saved = fontyourface_save_font($font, TRUE); // Here it gets saved in the database -> TRUE overrides existing fonts.
}
}
So, which method to use when?
If your custom module has only a small amount of configurations (like Views, Rules, Context, etc) the Ctools way is a great and standarized way to get you up to speed. It comes with a nice admin UI and the time to implement Features integration in your module using this method can be done within 30 minutes.
If your module has a lot of possible configurations (like hundreds) or if you use Views to list all available configurations – you might be better of without Ctools.
Thoughts? Comments? Please let me know in the comments!
Tags:
Drupal
Modules
Features
Planet Drupal
Ctools
Views
@font-your-face