Exporting your tables via features in a dozen lines

Stella's written an excellent blogpost on Using CTools in Drupal to make exportables, which feeds in so nicely to integrating your generic database table with Drupal Features that the module maintainers recommend it as an intro to hooking your module up through CTools and making database data exportable using it (there's also documentation in CTools itself but it's best used as a reference.)

Because Stella's post covers admin interfaces and helper functions, and has been made generic so people can adapt it, then it covers far more than just integration. So when I worked out just how tiny the absolute bare minimum you needed to integrate a database table with Features really was, I thought it was worth writing a blogpost which boils Features/CTools exportables down to their impressive distillates.

Here's my example. Imagine you're building a contacts module which just creates a database table through hook_schema. This table will be used to store your friends' contact details: at first, a unique nickname and their full name. The module does nothing more, so here's its contacts.info file:

name = Contacts
description = All my contacts
core = 6.x

And here's the contacts.module file:

// Empty file!

Tiny, isn't it? That's because all our clutter is in the database schema for now. So we use hook_schema to define the database schema, and hook_install/hook_uninstall to create and drop the table for our friends' information:

/**
 * Implementation of hook_install
 */
function contacts_install() {
  drupal_install_schema('contacts');
}
 
/**
 * Implementation of hook_install
 */
function contacts_uninstall() {
  drupal_install_schema('contacts');
}
 
/**
 * Implementation of hook_schema
 */
function contacts_schema() {
  $schema['contacts_friend'] = array(
    'description' => t('Friends'),
    'fields' => array(
      'nickname' => array(
        'description' => 'Unique nickname for friend',
        'type' => 'varchar',
        'length' => 32,
        'not null' => TRUE,
      ),
      'fullname' => array(
        'description' => 'Friend fullname',
        'type' => 'varchar',
        'length' => 128,
        'not null' => TRUE,
      ),
 
    'primary key' => array('nickname'),
  );
  return $schema;
}

This is all standard schema API stuff: if you're not sure what any of this means, check out the Schema API documentation for more information. Incidentally, good database maintainers might blanch at the idea of a text primary key. I feel your pain, but for this demo---and for a friendly Features admin interface---it's necessary, as we'll see below.

So now you've got your module. You haven't worried about features yet, but you suddenly realized you might want to export friends through Features, so you can import them into some other site. How do you do that? Well, you add nine lines of array code to your database schema: a new 'export' array key in the table schema with the following contents:

/**
 * Implementation of hook_schema
 */
function contacts_schema() {
  $schema['contacts_friend'] = array(
    'description' => t('Friends'),
 
    // This is all the CTools Exportables block
    'export' => array(
      // Unique key to identify an object
      'key' => 'nickname',
      // Object type identifier, e.g. "enemy" for your enemy exportables!
      'identifier' => 'friend',
      // Function hook name in Features dump
      'default hook' => 'default_contacts_myobj',
      'api' => array(
        // Which module "owns" these objects?
        'owner' => 'contacts',
        // Base name for Features include file
        'api' => 'default_contacts_friends',
        // Exportables API version numbers: black magic; woowoo
        'minimum_version' => 1,
        'current_version' => 1,
      ),
    ),
 
    'fields' => array(
      'nickname' => array(
        'description' => 'Unique nickname for friend',
        'type' => 'varchar',
        'length' => 32,
        'not null' => TRUE,
      ),
      'fullname' => array(
        'description' => 'Friend fullname',
        'type' => 'varchar',
        'length' => 128,
        'not null' => TRUE,
      ),
 
    'primary key' => array('nickname'),
  );
  return $schema;
}

And that's it, in the first instance. When you add some friends to that table---you'll have to do it with SQL or phpMySQL for the moment, unless you build an admin interface---they should automatically appear as exportables in the Features creation interface. The human-readable label will be their 'export' 'key', which is why we needed that key to be human-readable. You could have a unique serial ID as well, and impose a mere unique_key constraint on the nickname field, but Features don't work terribly well with serial IDs for now; you'd need to make sure CTools does not export that column.

Which leads me neatly onto the next example. Let's say you add a column that tells you whether you secretly hate each "friend" or not, and you don't want that getting exported out in case people find out what you think about them. This column is just an integer/boolean, so you add your field description array as usual... with one extra array key called, with similar boolean confusion as "not null", "no export":

/**
 * Implementation of hook_schema
 */
function contacts_schema() {
  $schema['contacts_friend'] = array(
    'description' => t('Friends'),
 
    'export' => array(
      // Unique key to identify an object
      'key' => 'nickname',
      // Object type identifier, e.g. "enemy" for your enemy exportables!
      'identifier' => 'friend',
      // Function hook name in Features dump
      'default hook' => 'default_contacts_myobj',
      'api' => array(
        // Which module "owns" these objects?
        'owner' => 'contacts',
        // Base name for Features include file
        'api' => 'default_contacts_friends',
        // Exportables API version numbers: black magic; woowoo
        'minimum_version' => 1,
        'current_version' => 1,
      ),
    ),
 
    'fields' => array(
      'nickname' => array(
        'description' => 'Unique nickname for friend',
        'type' => 'varchar',
        'length' => 32,
        'not null' => TRUE,
      ),
      'fullname' => array(
        'description' => 'Friend fullname',
        'type' => 'varchar',
        'length' => 128,
        'not null' => TRUE,
      ),
      'do_i_hate' => array(
        'description' => 'Do I secretly hate this person?',
        'type' => 'int',
        'not null' => FALSE,
        'default' => 0,
        'no export' => TRUE,
      ),
 
    'primary key' => array('nickname'),
  );
  return $schema;
}

Result? You can still pickle friends into a Feature---if you like that sort of thing---but the contents of any "no export" columns will not be present in the final Feature bundle.

And that's it. You can build interfaces and applications on top of this, but that's all you need to get your database contents up and running through features. All these code snippets should be runnable; that is, you can save them and run them as live code. If you can't, please let me know and I'll fix.

Comments

The code implements hook_install() twice, i think you meant hook_uninstall() the second time.

 

Also, I got my exportables working by inserting the 9 lines of code into the hook_schema() like you said (modified to work on my table), and then I went and created a feature. The feature code looks good, it has the data in the code as objects. BUT when I enable the feature, my module's table doesn't change. It's as if the data's not getting properly imported back when I enable the feature, any ideas?

 

Maybe my module needs to do an extra step in order to import the data?

 

Cheers!

Sorry about that. I've only just discovered yours.

Thanks for the feedback; I've fixed the hook_(un)install now. You're right that you do need some code to re-import the feature exports: my test environment was corrupt and I thought they were getting re-imported when they weren't. I'll write a basic follow-up post when I can but for now if you look at my Features Conf Wizard module on github then you might be able to find some tricks on how to do the re-import.