Exportable configuration with CTools – revisited

A while ago I wrote about how to provide exportable configuration presets in Drupal modules. While I was taking advantage of CTools' exportables API rather than building this functionality from scratch, we still ended up with quite a lot of code for what was supposed to be a standardized feature. Since then, however, CTools' exportables API has evolved and now also provides a UI out of the box.

In this article, I'll do a similar walkthrough as I did in my earlier post of how to implement a configuration preset storage in your module and make it exportable with CTools. Only this time, the user interface will be provided automatically by CTools, which means there will be a lot less code. As a maintainer of the FFmpeg Converter module, this is a big win since the amount of code I have to maintain is reduced significantly.

Configuration presets, as I call them in this article, are collections of settings that are bundled up and used in different situations on a site. The screenshot above shows FFmpeg Converter's list of presets in the administration section of a Drupal site. A better-known example is ImageCache and its presets that allow you to set up groups of customized image manipulations that can then be picked up and used on different parts of the site.

To give a quick explanation of what exportables are, here's a quote from the old, now deprecated article:

There is a well known problem with configuration driven development, which is the way things work in Drupal most of the time. You can quickly set up large portions of a site just by clicking around in the admin interface. However, when it comes to doing things like deploying new features on running live sites, things get trickier. This is because there is no clean separation between the content and the configuration on the site. /.../ The solution to this problem, most of the time, is to capture the configuration in code somehow. The code can then be version controlled and distributed between different sites, or different versions of a site...

CTools has quickly become the workhorse of choice for managing this kind of exportability. So, let's take a look at how it's done.

Getting started

As in the previous article, I'll use FFmpeg Converter as an example of how to utilize Ctool's APIs for making modules' configuration presets importable and exportable. (In fact, a good part of this post will be just a straight copy.) This time though, we won't have to create an interface for adding, removing, editing and reverting these presets – CTools does it for us.

There are a couple of references you can check while you read this if you need more information:

  • The export.html and export-ui.html help files in your CTools module directory.
  • api.drupal.org and/or drupalcontrib.org for learning about different API functions.
  • Stella Power has also written a blog post on the subject of CTools exportables, although that doesn't include the Export UI.

Defining your data

Just as before, we start by defining the table structure for the presets. CTools requires that the exportables live in their own database table and have at least a machine readable name field, and a database-only numeric primary key. This is what FFmpeg Converter's preset table looks like:

name          pid  description        ffmpeg_wrapper
-----------------------------------------------------
default_h264  41   Default H264       ...
example_1     44   An example preset  ...

As always in Drupal, the table structure is defined in the ffmpeg_converter.install file:

<?php

/**
* Implementation of hook_schema().
*/
function ffmpeg_converter_schema() {
 
$schema['ffmpeg_converter_preset'] = array(
   
'description' => t('Table storing preset definitions.'),
   
'export' => array(
     
'key' => 'name',
     
'identifier' => 'preset', // Exports will be as $preset
     
'default hook' => 'default_ffmpeg_converter_preset'// Function hook name.
     
'api' => array(
       
'owner' => 'ffmpeg_converter',
       
'api' => 'default_ffmpeg_converter_presets'// Base name for api include files.
       
'minimum_version' => 1,
       
'current_version' => 1,
      ),
     
'load callback' => 'ffmpeg_converter_preset_load', // Use this function to load presets (optional).
   
),
   
'fields' => array(
     
'name' => array(
       
'type' => 'varchar',
       
'length' => '255',
       
'description' => 'Unique ID for presets. Used to identify them programmatically.',
      ),
     
'pid' => array(
       
'type' => 'serial',
       
'unsigned' => TRUE,
       
'not null' => TRUE,
       
'description' => 'Primary ID field for the table. Not used for anything except internal lookups.',
       
'no export' => TRUE, // Do not export database-only keys.
     
),
     
'description' => array(
       
'type' => 'varchar',
       
'length' => '255',
       
'description' => 'A human readable name of a preset.',
      ),
     
'ffmpeg_wrapper' => array(
       
'type' => 'text',
       
'size' => 'big',
       
'description' => 'A serialized array of FFmpeg Wrapper options.',
      ),
    ),
   
'primary key' => array('pid'),
   
'unique keys' => array(
     
'name' => array('name'),
    ),
  );
  return
$schema;
}


/**
* Implementation of hook_install().
*/
function ffmpeg_converter_install() {
 
drupal_install_schema('ffmeg_converter');
}

?>

That looks more or less like a standard Drupal install file, except for the export part of the schema array. This is specific to Ctool's export API, and it defines how this table's content will be imported and exported. This is the basic thing that is needed for the information to be exportable.

CRUD functions

How about the standard CRUD (create, read, update, delete) functions that you usually use to handle your objects? The truth is, with CTools Export UI you don't actually need them for the configuration UI itself. They may still be convenient, and the good news is that CTools has a complete set already defined, such as ctools_export_crud_load() etc.

FFmpeg Converter has its own _load function defined since it does a couple of extra steps with the preset when it's loaded. (We used to *need* to define this function in order to use CTools exportables, but that was before the Export UI.)

<?php
/**
* Load a preset.
*
* @param $name
*   This presets's name value.
* @return
*   An array of options for the specific preset.
*/
function ffmpeg_converter_preset_load($name) {

 
// Use CTools export API to fetch this preset.
 
ctools_include('export');
 
$result = ctools_export_load_object('ffmpeg_converter_preset', 'names', array($name));
  if (isset(
$result[$name])) {
   
// Sanitize the preset data.
   
ffmpeg_converter_preset_sanitize($result[$name]);
    return
$result[$name];
  }
}
?>

To make the CTools Export API use this function rather than ctools_export_crud_load() when loading preset objects, I've added this function as load callback in the export part of the hook_schema() declaration above. This is all optional however, so you can just omit load callback if you don't need this.

Defining default presets

In order to provide some default configuration, you can implement a couple of hooks that CTools provides. These presets will show up in the admin interface without the user having to do anything other than activating the module. This is exactly how third party modules would do to provide their own default, code defined, presets. It is also how Features saves preset data in code.

<?php

/**
* Implementation of hook_ctools_plugin_api().
*
* Tell CTools that we support the default_ffmpeg_converter_presets API.
*/
function ffmpeg_converter_ctools_plugin_api($owner, $api) {
  if (
$owner == 'ffmpeg_converter' && $api == 'default_ffmpeg_converter_presets') {
    return array(
'version' => 1);
  }
}

/**
* Implementation of hook_default_ffmpeg_converter_preset().
*
* Provide a couple of default presets.
*/
function ffmpeg_converter_default_ffmpeg_converter_preset() {
 
$export = array();

 
$preset = new stdClass;
 
$preset->api_version = 1;
 
$preset->name = 'default_flv';
 
$preset->description = 'Default FLV preset';
 
$preset->ffmpeg_wrapper = ffmpeg_converter_default_options();
 
$export['default-flv'] = $preset;

  return
$export;
}

?>

The administration interface

Time to take a look at the parts that really differ from before. Instead of defining a number of page callbacks and then start building pages for listing, adding/editing, deleting, importing and exporting presets, we just expose our preset schema to the Export UI. We do this by telling CTools that we want to define an 'export_ui' plugin:

<?php

/**
* Implementation of hook_ctools_plugin_directory().
*/
function ffmpeg_converter_ctools_plugin_directory($module, $type) {

 
// - Abbreviated. - //

  // Load the export_ui plugin.
 
if ($type =='export_ui') {
    return
'plugins/export_ui';
  }
}

?>

After that we need to create the plugin directory plugins/export_ui in the module directory. Within this directory, we place the main plugin file called ffmpeg_converter_ctools_export_ui.inc. The directory structure should look like this:

ffmpeg_converter.module
plugins/
-  export_ui/
-  -  ffmpeg_converter_ctools_export_ui.inc

In this file, we and add the plugin definition:

<?php

/**
* Define this Export UI plugin.
*/
$plugin = array(
 
'schema' => 'ffmpeg_converter_preset',
 
'access' => 'administer ffmpeg wrapper',
 
'menu' => array(
   
'menu item' => 'ffmpeg_converter',
   
'menu title' => 'FFmpeg Converter',
   
'menu description' => 'Administer FFmpeg Converter presets.',
  ),

 
'title singular' => t('preset'),
 
'title plural' => t('presets'),
 
'title singular proper' => t('FFmpeg Converter preset'),
 
'title plural proper' => t('FFmpeg Converter presets'),

 
// - Abbreviated. - //

 
'form' => array(
   
'settings' => 'ffmpeg_converter_ctools_export_ui_form',
   
'submit' => 'ffmpeg_converter_ctools_export_ui_form_submit',
  ),
);

?>

Starting at the top, we begin by defining which database schema this plugin deals with. Then we provide the permission that should be required for users to administer the presets, followed by a definition of the menu link which will point to the admin page. Below that are a couple of string definitions that will be used in user texts on the administration pages.

Finally the 'form' array defines the name of the function that generates the preset add/edit form, as well as form validation and submit functions. We put all of these form functions in the same plugin file:

<?php

/**
* Define the preset add/edit form.
*/
function ffmpeg_converter_ctools_export_ui_form(&$form, &$form_state) {
 
$preset = $form_state['item'];

 
$form['description'] = array(
   
'#type' => 'textfield',
   
'#title' => t('Description'),
   
'#description' => t('The human readable name or description of this preset.'),
   
'#default_value' => $preset->description,
   
'#required' => true,
  );

 
// Add FFmpeg Wrapper's configuration form.
 
$form += ffmpeg_wrapper_configuration_form($preset->ffmpeg_wrapper, "edit-ffmpeg-converter-presets-$preset->name-ffmpeg-wrapper-");
 
$form['ffmpeg_wrapper']['#collapsible'] = true;
 
$form['ffmpeg_wrapper']['#tree'] = true;
}

/**
* Submit handler for the preset edit form.
*/
function ffmpeg_converter_ctools_export_ui_form_submit($form, &$form_state) {
 
// Flatten and serialize the ffmpeg_wrapper settings array.
 
$form_state['values']['ffmpeg_wrapper'] = serialize(_ffmpeg_converter_array_flatten($form_state['values']['ffmpeg_wrapper']));
}

?>

And that's it! If all went well there should now be a complete admin interface looking similar to the image at the top, located at admin/build/ffpeg_converter in this case.

A couple of things have changed compared to the interface in the previous article. For instance, the admin page now resides under Admin > Build rather that Admin > Settings (update: this can be overridden in the plugin), and there is now an advanced – perhaps too advaced – interface for filtering the preset list page.

If you want to do customizations of how the UI looks and behaves, that's certainly possible. The way to do that is to add a 'handler' key to the $plugin definition where you define your own export_ui handler. This hander would be a class that inherits ctools_export_ui and only contains the actual overrides that you want to do. Custom handlers are out of scope for this article, but a working example can be found in the 6.x-2.x version of FFmpeg Converter.

Conclusion

I hope this can provide a good starting point for module developers who want to make their configuration presets a part of the whole exportables paradigm. With CTools' Export UI, this previously daunting task can now be achieved with a minimum of effort and in a stable, standardized fashion.

Kommentarer

Thanks a lot for writing this up! I think this is a nice look at what Export UI provides and will give users who haven't looked into it at all a taste of the power they can have.

A couple of things have changed compared to the interface in the previous article. For instance, the admin page now resides under Admin > Build rather that Admin > Settings, and there is now an advanced – perhaps too advaced – interface for filtering the preset list page.

This can be modifed by setting 'menu prefix' to 'admin/settings'. It defaults to 'admin/build' which is where the majority of UIs like this will live. In the case of your module, settings may actually be better. Change that setting and force a menu rebuild and your menu will move.

and there is now an advanced – perhaps too advaced – interface for filtering the preset list page.

This can probably be cut down if you override the list methods, but it's probably not worth it. My experience is that once users see what the search widgets do, they basically stop seeing them.

I guess you're the ultimate proof reader for this article, so I'm glad you liked it! Made a small addition about the menu prefix and corrected the capitalization. Thanks!

Hello folks ! Very well written article, thank you.
Since it's rather complete and has been proof read by merlinofchaos himself, may I suggest you to copy paste the whole article in a new doc page in drupal.org ? Even if it's a bit specific to FFmpeg converter module, it covers the overall functionality, and it's far better than no doc at all !

Yes, I've been thinking about that. I'd like to make it a little more generalized, even though you're right that it would be better than nothing even as it is. Please give me a day or two!

Lägg till ny kommentar

More Tech

The appification of the web

Recently it seems like we're at a tipping point, where a new generation of the web is being born. One aspect of this is the “appification” of the web. In this post I’ll try to give a bird’s eye view of this development.