Making Your Module Features Exportable

One of the problems that Drupal sites have traditionally had was how to migrate configuration and data from one environment to another. This was especially annoying when we're talking about basic menus, views and content. Enter Features!

Features

! With Features, you can now export many of the key components of any site and easily port their configuration from one site to another, without ever having to do a dreaded manual recreation. Within core features, you can export dependencies, menus, content types, roles, permissions and views. Modules such as the uuid_features module allow you to export content, taxonomy and vocabularies. But what if there's something specific within your module that you'd like to make features able to export? Please note, there are some helpful code samples and documentation available within files of the Features module, specifically api.txt and features.api.php.

Configure Features to recognize our module

To start with, we need to notify Features that we intend to interface with it by invoking HOOK_features_api(). This should go into your .module file (For example, mymodule.module).

  1. /**
  2.  * Implementation of hook_features_api
  3.  *
  4.  * Here we define the components that we want to make exportable.  For this
  5.  * particular module, we want to make the configurations exportable.
  6.  */
  7. function mymodule_features_api() {
  8.   return array(
  9.     'mymodule_config' => array(
  10.       'name' => 'My Module Configs',
  11.       'file' => drupal_get_path('module', 'mymodule') .'/mymodule.features.inc',
  12.       'default_hook' => 'mymodule_config_features_default_settings',
  13.       'feature_source' => TRUE,
  14.     ),
  15.   );
  16. }

This function will notify Features of the component that you want to make available, and where you plan to do so. This function needs to return a multi-dimensional array of configurations. In our example, we only have a single component, mymodule_config. The name mymodule_config will be known to Features as the component name, this can be entirely different from your module name, and might be easier to keep track of your code if you do so. Note: If your module has another type of data or configuration that it needs to make available, then you can have a second element in the outer array, with a different component name.

  • File - This tells Features where we are going to be putting all of our Features-related code and therefore to include it at the appropriate times.
  • Default Hook - This is the function that will get called when a feature is enabled (we'll get to this later)
  • Feature Source - This tells Features to make this component available when first creating a feature from scratch

Alert Features what is available to be exported

At this point, we've just told Features that we're going to allow users to export a component named 'My Module Configs' but if we were to go to the create features page (admin/build/features/create) and look under 'Edit Components' we won't see ours… why? The second thing we'll want to do is tell Features what are the available instances of this component that can be exported. We do this by implementing the Features hook _features_export_options. This does not leverage the name of the module, but rather the component name (ie. mymodule_config) you provided as the array index in hook_features_api(). Note: All the functions below this point should be contained in the 'file' attribute of hook_features_api (ie. mymodule.features.inc)

  1. /**
  2.  * Implementation of hook_features_export_options. [component_hook]
  3.  *
  4.  * This hook will alert features of which specific items of this component may
  5.  * be exported. For instances, in this case, we want to make available all the
  6.  * existing items.  If there are no items to be exported, this component will
  7.  * not be made available in the features export page.
  8.  *
  9.  * @return array
  10.  *   A keyed array of items, suitable for use with a FormAPI select or
  11.  *   checkboxes element.
  12.  */
  13. function mymodule_config_features_export_options() {
  14.   $options = array();
  15.   $query = " SELECT sys_name, name FROM {mymodule_table} ";
  16.   $params = array();
  17.   $result = db_query($query, $params);
  18.  
  19.   while ($row = db_fetch_array($result)) {
  20.     $options[$row['sys_name']] = $row['name'];
  21.   }
  22.   return $options;
  23. }

This module should expose the items which you want to make exportable to the user. The end result will be an array which results a name/value pair, much like you would do in any select pull down.

Now, if you go back to the create features page, and reload, you should see your component listed (assuming you have data to export). At this point, we can select one of the items we want to export.

Include specifics about what else is required

When we select an item to export, what needs to be included? This is where the hook, COMPONENT_features_export(…) comes in.

  1. /**
  2. * Implementation of hook_features_export [component hook]
  3. *
  4. * This is a component hook, rather then a module hook, therefore this is the
  5. * callback from hook_features_api which relates to the specific component we
  6. * are looking to export. When a specific instance of the component we are
  7. * looking to export is selected, this will include the necessariy item, plus
  8. * any dependencies into our export array.
  9. *
  10. * @param array $data
  11. * this is the machine name for the component in question
  12. * @param array &$export
  13. * array of all components to be exported
  14. * @param string $module_name
  15. * The name of the feature module to be generated.
  16. * @return array
  17. * The pipe array of further processors that should be called
  18. */
  19. function mymodule_config_features_export($data, &$export, $module_name) {
  20. //we have module dependencies in order for this module to function properly
  21. //so we'll add them here
  22. $export['dependencies']['mymodule'] = 'mymodule';
  23.  
  24. // The following is the simplest implementation of a straight object export
  25. // with no further export processors called.
  26. foreach ($data as $component) {
  27. $export['features']['mymodule_config'][$component] = $component;
  28. }
  29. return array();
  30. }

There are three parameters in this hook. They are:

  • Data - this will be an array of items selected from this component. Basically you will get back the data you made available in _features_export_options().
  • Export - this is essentially the array or log of information features will use to keep track of what it needs to export as part of any package. In this function, we'll be adding to this array as a result of the user selecting items to export from our module
  • Module Name - this is the name of the feature the user is looking to create

Since our code relies on our module in order to function, we'll make mymodule a dependency just in case the user doesn't select it on their own. We could just have easily made another Features-exportable component required, such as a view or a taxonomy. After this, we loop over the $data array in order to get the instances that the user wants to export. In this case we're just going to add the primary key or system name (sys_name) to the array so that we can retrieve the specific information on it later. Note the format of this array here:

$export['features']['<component_name>'][<key-or-sysname>] =<key-or-sysname>;

Export our data

Now that we've notified features we'll be making a component available, and exposed which items are exportable, how do we make it do the export when the user presses the 'Download feature' button? If we were to download the feature now, Features would do its thing, but as far as our component, there wouldn't be much to show. In order for Features to know how to export our data, we need to implement the hook COMPONENT_features_export_render(…)

Function mymodule_component_features_export_render($module_name, $data, $export = NULL)

This is the hook that Features will be using to generated the information we want it to write out to code, which will in turn be used to import the configuration or data in other places. The declaration here is similar to the previous function, but be sure to note the difference in the order of the parameters.

  1. /**
  2.  * Implementation of hook_features_export_render. [component hook]
  3.  *
  4.  * This hook will be invoked in order to export
  5.  * Component hook. The hook should be implemented using the name ot the
  6.  * component, not the module, eg. [component]_features_export() rather than
  7.  * [module]_features_export().
  8.  *
  9.  * Render one or more component objects to code.
  10.  *
  11.  * @param string $module_name
  12.  *   The name of the feature module to be exported.
  13.  * @param array $data
  14.  *   An array of machine name identifiers for the objects to be rendered.
  15.  * @param array $export
  16.  *   The full export array of the current feature being exported. This is only
  17.  *   passed when hook_features_export_render() is invoked for an actual feature
  18.  *   update or recreate, not during state checks or other operations.
  19.  * @return array
  20.  *   An associative array of rendered PHP code where the key is the name of the
  21.  *   hook that should wrap the PHP code. The hook should not include the name
  22.  *   of the module, e.g. the key for `hook_example` should simply be `example`.
  23.  */
  24. function mymodule_config_features_export_render($module_name, $data, $export = NULL) {
  25.   $code = array();
  26.   $code[] = '  $myconfigs = array();';
  27.   $code[] = '';
  28.   foreach ($data as $sys_name) {
  29.     //retrieve the contest information
  30.     $item = _mymodule_config_get_data($sys_name);
  31.     //add the contest code to the feature
  32.     if (isset($items[$sys_name])) {
  33.       $code[] = '  $myconfigs[] = '. features_var_export($item, '  ') .';';
  34.     }
  35.   }
  36.   $code[] = '  return $myconfigs;';
  37.   $code = implode("n", $code);
  38.   return array('mymodule_config_features_default_settings' => $code);
  39. }

A few things to note within this function: The ultimate goal of this function is to dynamically create a snippet of php code that will be used during the install process to duplicate the configurations we're exporting here. This code snippet will be exported to the Features include file. Similarly to how we looped over the instances the user selected to export in the previous function, we do so here, but this time we actually use the key that was included to retrieve the data necessary in order to recreate this item. We'll retrieve all the data we need to migrate and make an array structure out of it. This array structure will then be passed into the features_var_export function which will format it nicely for output. Finally, in the final return statement of this function, there is a name/value pair which is vital. The name being used here should be the name of the hook you'll be looking for during the import process (discussed below). Note: we also set this in the configuration of hook_features_api above. At this point, if you did everything correctly, and you press the 'Download features' button and look at the feature that got created, you should find the export of data for the items of your module that you selected.

Rebuild our data

In order to tell features how to recreate an item from the code snippet we created in the previous function, we'll need to implement COMPONENT_features_rebuild(…)

  1. /**
  2. * Implementation of hook_features_rebuild(). [component_hook]
  3. */
  4. function mymodule_config_features_rebuild($module) {
  5. $items = module_invoke($module, 'mymodule_config_features_default_settings');
  6. //loop over the items we need to recreate
  7. foreach ($items as $idx => $item) {
  8. //basic function to take the array and store in the database
  9. $saved = _mymodule_config_set_data($item);
  10. }//end - loop
  11. }

This function will get called when a feature is installed. Quite possibily the most important part of this function is the very first line.

$items = module_invoke($module, 'mymodule_config_features_default_settings');

In this line, the second parameter should match the name that you added to the return statement of the previous function. This will serve as a hook, looking through the feature which is being installed to find anything that matches, and return the data within it to be parsed and handled. In our example, we retrieve the array of data that we exported then loop over it and write to the database.

Revert

Reverting occurs when someone makes a change to the settings directly on the site, but we want to go back to the configuration that existed in code. Often times this can be identical to the rebuild stage; this is the case in our example. Since there's no difference between the two implementations, we will implement the hook COMPONENT_features_revert(...) but it simply makes a call to the rebuild function declared previously.

  1. /**
  2.  * Implementation of hook_features_revert(). [component_hook]
  3.  */
  4. function mymodule_config_features_revert($module) {
  5. mymodule_config_features_rebuild($module);
  6. }

 

There's a lot more that's possible in the features universe, but hopefully this helps get you started with making your module exportable.
But wait, there's more! Stay tuned for Neil Hastings' take on approaching the same challenge with CTools. Follow us on Twitter or Facebook to get alerted to the latest from the Treehouse team!

twysocki