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:

  1. Define the data you want to export by expanding your hook_schema() with an ‘export’ part.
  2. Write a load() function
  3. 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!

Next post

Goodbye Google Fonts, hello @font-your-face!

Read More »

Comments

exportable entities...

Rules (and others) doesn't make use of ctools exportables, it's using exportable entities provided by the entity api module. Leaving the old "should configuration be an entity" discussion beside, it allows you to use Views for exported entities as they are synced into the database. Moreover, it generates the Views integration for you.

Also, it comes with Features integation - which though is implemented in a separte controller class you can easily override and customize, without having to re-do all on your own. So I guess this should be option 3.

Thanks for the explanation

Sounds like a great solution, but I assume you have to use entities for your configuration then? The @font-your-face fonts are no entities (yet) - so I believe this might be even more work in our situation. But good to know there is already a solution for modules that are built on entities. Thanks :)

Seeking some clarification

Could you please specify what version of Drupal this applies to? Perhaps it would work on D6 and D7, but I don't know enough about the differences between the two to determine it on my own.

Also, is there a version of @font-your-face that has Features functionality built in? Or should folks looking for this functionality built their own module that enables Features support using your examples? Perhaps you've already created a module that extends @FYF...

Otherwise, it's a very clear example of how to incorporate features support for probably any module. Thank you for writing up such insightful and helpful documentation. Sometimes it's difficult to come by.

Drupal7

Hi Jason,

this post applies to Drupal7. This post is the outcome of my work to integrate Features compatibilty in @font-your-face. Version 7.x-2.x has this included (see here). 

Thanks for clearing that up.I

Thanks for clearing that up.

I have potential good news. I decided to just try plopping the Features code from 7.x-2.1 into 6.x-2.10 and it seems to have just worked. These are just my initial findings.

I brought over the fontyourface.features.inc and copied the fontyourface_features_api function into the fontyourface.module. When I tried to create a Feature the drop down was available. It autodetected the TypeKit API token and my Fonts were listed for me to check. Seemed A-OK. I exported the feature and reverted my settings to a pre-@FYF configuration. Then I refreshed to ensure everything was unset and my fonts went away. When I enabled the new Feature, my fonts started working again!

There may be some functionality that I am not leveraging that may be incompatible, but I did not come across any actual issues with my use case. I will share this code with my team and see if they also experience success enabling the feature with the modified 6.x-2.10 module. I'll report back when I'm more confident that this actually did work.

Add new comment | Baris Wanschers

Geweldig bericht! Je hebt me enorm uit de brand geholpen. Ook jouw manier van schrijven trekt mij enorm. Schrijf aub meer over dit onderwerp! Laat me het aub weten want ik ben echt nog op zoek naar meer van dit. Dus link maar door als je hier nog meer van hebt. Zelf schrijf ik ook voor meerdere blogs, dus als iemand daar interesse in heeft of bij mij ook een berichte achter wilt laten dan kan dat! Mijn blogs hebben een ontzettend gevarieert publiek dus om deze mensen zo goed mogelijk te benaderen is het hebben van een goed lezende schrijstijl enorm van belang. Kijk anders ook eens op een van mijn blogs en laat hier je tips of opmerkingen achter.

Ctools Export API allows you

Ctools Export API allows you override the list callback and load all callback, so it would have actually been pretty easy to filter out only the enabled configuration on bulk export UI (and immediately on features UI as well, since that is going to call the same function) and thus saving you a lot of code :)

As for the views problem. Haven't looked at fontyourface in a while (mostly during development of sweaver), but Export UI is really easy to extend (and the default one has nice default filters as well) so it should/could work, but I should see the UI. Ironically, since you're using views, you're automatically dependent on ctools as well (at least in D7).

Granted, I'm not features fan at all.