Taxonomy Colour: Drupal Module Development Tutorial (Part Two)

4th July 2011

In the first part, I looked at creating the database schema for a simple Drupal module designed to allow you to associate colours with taxonomy terms. In this second part, I'll look at the administration aspects of the module. In essence, what we need to do is as follows:

  • Since colour-coding may only be appropriate to certain vocabularies (e.g. categories) and not others (tags, perhaps). We need to provide the user with the option to specify which vocabularies are applicable.
  • The user needs to be able to specify a colour when adding or editing a term, where appropriate.
  • We need to provide a mechanism which allows the colour associated with a term to be stored, retrieved, and to be displayed.

Later, I'm going to want to incorporate the colour in views, as well as consider some performance aspects. The next thing, then, is to start coding the module and this will take place in the PHP file we created in part one, taxonomy_color.module.

Introducing hook_form_alter

To allow the option to categorise terms on a per-vocabulary basis, we'll add a checkbox to the edit vocabulary form (which is also used to add a new vocabulary). We achieve this by harnessing the power of hook_form_alter. In order to intercept the form and add this checkbox, we need to create a method whose name matches the signature hook_form_FORM_ID_alter. The first part of this tutorial introduced hooks, and I introduced the idea that the the hook part should be replaced by the ID of the module, thus: taxonomy_color_form_FORM_ID_alter. To find our form ID, the easiest way is to get the form on screen by navigating to the relevant page (in this case, go and edit an existing term), view source and check the ID of the form tag:

<form action="/admin/content/taxonomy/edit/vocabulary/1"  accept-charset="UTF-8" method="post" id="taxonomy-form-vocabulary">

You'll see that this is taxonomy-form-vocabulary - we must then, however, get rid of those dashes and replace them with underscores, i.e. taxonomy_form_vocabulary - so the method name becomes: taxonomy_color_form_taxonomy_form_vocabulary_alter Quite a mouthful, huh? This mechanism for determining your function name might seem unwieldy and complicated, but you soon get the hang of it. Consulting the documentation / source for hook_form_FORM_ID_alter shows that the method should look like this:

/**
 * Implementation of hook_form_FORM_ID_alter().
 */
function taxonomy_color_form_taxonomy_form_vocabulary_alter(&$form, &$form_state) {
 // implementation goes here
}

So, let's start implementing the function by adding the form element.

    $form['color'] = array(
          '#type' => 'checkbox',
          '#title' => t('Use colour-coding'),
          '#default_value' => $checked,      
        );

Once you've added this new element, the form should look something like the figure below:

The Add / Edit Vocabulary form, after we've added a new checkbox ("Use colour-coding")

You'll probably notice a minor issue immediately - the new element sits below the Save (i.e. the submit) and Delete buttons. Whilst the form is still fully functional, it' not ideal from a usability point-of-view. In order to fix this we juggle the weights, which control the order in which elements appear within a form. Let's first set the weight - configured as an element of the array that represents the element - to a high number, thus:

  $form['color'] = array(
    '#type' => 'checkbox',
    '#title' => t('Use colour-coding'),
    '#default_value' => $checked,      
    '#weight' => 999, 
  );

And then change the weights of the Save (the submit) and Delete buttons to greater values (the greater the value, the further down the form the element will appear), thus:

    $form['submit']['#weight'] = 1000;
    $form['delete']['#weight'] = 1001;

The form should now be re-ordered, as illustrated by the figure below:

The amended form, now we've juggled the weights around

I won't go into the mechanics of the Drupal Form API - you may wish to refer to the Form API Quickstart Guide - but we're basically adding a new element called color, which is a checkbox with the given caption (the t() function - which stands for translate - allows us to localise our titles and messages). Now, the other element which so far I've just skimmed over - the default_value element. In truth this is probably easier to think of as an initial value; when adding a new vocabulary it's a default value but when editing it's the current value. But before we can set this value, we need to decide where it' going to get stored.

Using Variables in Drupal

Creating a table to hold these values is probably overkill, however Drupal provides simple key-value storage via the variable system, so that's what we'll do. What I'm going to do, then, is simply have a variable for each vocabulary indicating whether to apply a colour scheme or not. Sticking with the convention of prefixing everything with the module name to ensure it's unique, I've stumped for taxonomy_color_color_vocab_vid, where vid is the vocabulary ID. So the value of $default is determined as follows:

    if (isset($form['vid']['#value'])) {
        $checked = variable_get('taxonomy_color_color_vocab_'.$form['vid']['#value'], 0);
    } else {
        $checked = 0;
    }

If we're editing a vocabulary (rather than adding), the vid is stored in $form['vid']['#value'] - do a var_dump() to see this - so in the first case, we find out the current value for the given vocabulary, first appending the vid to the key. The second argument to the function variable_get specifies the value to return if the key in question doesn't have a value - 0 (false) seems a sensible default. If the vid isn't set then it- a new vocabulary, so let' set it to false.

Using hook_taxonomy

Next up, this value needs to be stored at the appropriate time - in this case when the form gets submitted; however there's a trick we can use to "hook in" to this part of the process; when a vocabulary is inserted or updated. We're going to use another hook - hook_taxonomy():

/**
 * Implementation of hook_taxonomy().
 */
function taxonomy_color_taxonomy($op, $type, $form_values = NULL) {

This function gets called when inserting, updating or deleting a vocabulary or a term. $op indicates the operation (insert, update or delete), and the second argument - $type - indicates whether the entity in question is either a term or vocabulary. Here's the implementation:

    switch ($type) {

        case 'vocabulary':
            switch ($op) {
                case 'insert':
                case 'update':
                    variable_set('taxonomy_color_color_vocab_'.$form_values['vid'], intval($form_values['color']));
                    break;
                case 'delete':
                    variable_del('taxonomy_color_color_vocab_'.$form_values['vid']);
                    break;
            }

            break;

We're interested in vocabularies right now - but we'll use a switch here because in a moment, we'll be looking at terms. Another switch decides what to do based on the operation; setting the variable we defined previously on an insert or an update, and if we're deleting a vocabulary then let's keep things tidy by deleting the corresponding variable.

Assigning Colour to Taxonomy Terms

Next up, we need to add the option to specify a colour for a given taxonomy term by adding a new field to the term edit form. Again, we achieve this using hook_form_alter, adding a new fieldset containing a textfield. Ideally we'd probably want to use JQuery to transform this element into a colour picker, but that's outside the scope of this tutorial. So, once again we have a look at the form to get the form ID:

<form action="/admin/content/taxonomy/edit/term/123"  accept-charset="UTF-8" method="post" id="taxonomy-form-term" enctype="multipart/form-data">

So by applying the same logic as before, our function name becomes: taxonomy_color_form_taxonomy_form_term_alter By inspecting the form we can see that the term ID (tid) is stored in $form['tid']['#value'], so we can check for a colour associated with that term ID, otherwise defaulting to black (hex 000000). This is shown in the snippet below.

$tid = $form['tid']['#value'];
  if (is_numeric($tid)) {
    $color = taxonomy_color_get_term_color($tid);
  } else {
    $color = '000000';
  }

You'll probably notice immediately that I've called a function which I haven't yet defined; its purpose should be self explanatory. Let's dive in:

function taxonomy_color_get_term_color($tid) {
  $result = db_fetch_object(db_query("SELECT color FROM {term_color} WHERE tid = '%d'", $tid));
  if (is_object($result)) {
    return $result->color;
  } else {
    return NULL;
  }
}

The SQL should be fairly clear; we run that query, substituting the provided term ID (tid) and fetch the result as an object. The db_query function provides an API to our database. Note that the table name is wrapped in curly braces; if a table prefix was allocated upon installation, this will be inserted for us - it's not safe to just assume the table is called term_color. And here is the rest of taxonomy_color_form_taxonomy_form_term_alter

/**
 * Implementation of hook_form_FORM_ID_alter().
 */
function taxonomy_color_form_taxonomy_form_term_alter(&$form, &$form_state) {
$vid = intval($form['#vocabulary']['vid']);

    if (variable_get('taxonomy_color_color_vocab_'.$vid, 0)) {  
  $form['submit']['#weight'] = 99;
  $form['delete']['#weight'] = 100;

  $tid = $form['tid']['#value'];
  if (is_numeric($tid)) {
    $color = taxonomy_color_get_term_color($tid);
  } else {
    $color = '000000';
  }

  $form['extras'] = array(
    '#type' => 'fieldset',
    '#title' => t('Extras'),
    '#collapsible' => TRUE,
  );

  $form['extras']['color'] =array(
    '#type' => 'textfield',
    '#title' => t('Color'),
    '#size' =>  6,
    '#default_value' => $color,
    '#description' => t('The Color that represents this term.'),
  );
  }
}

Note that before we modify the form, we check whether the vocabulary of this term uses colour-coding; whether editing or adding a brand new term the vocabulary is, by design, already specified and is stored in $form['#vocabulary']['vid'] so we use this to check the value of the relevant variable.

Using the Database in Drupal

So, that's the form modified, but we still need to store the colour in the table we created. Here we'll go back to our implementation of hook_taxonomy(), and add a clause to the switch statement to pick up on term changes.

function taxonomy_color_taxonomy($op, $type, $form_values = NULL) {
  ...
  case 'term':
` If the form contains a term ID (tid), we check the operation being performed, which is stored in `$op`. The function continues as follows: `
  if (isset($form_values['tid'])) {
    // grab the tid  
    $tid = $form_values['tid'];
    switch ($op) {
` If the term is being inserted or updated, `$op` will be **insert** or **update** respectively, but we'll handle them with the same function: `
    switch ($op) {
      case 'insert':
      case 'update':
        // if we're inserting or updating, set the color
        if (!empty($form_values['color'])) {
          taxonomy_color_add($tid, $form_values['color']);
        }        
        break;
` There's a new function here - `taxonomy_color_add` - but let's just finish this function before addressing that: `
      case 'delete':
        // delete the appropriate row in term_color
        taxonomy_color_delete($tid);
        break;
    }

So as you can see, the next step is to define functions to add / update / delete a colour for a given term. Deleting is easy:

/**
 * Delete the color associated with a given term
 */
function taxonomy_color_delete($tid) {
  return db_query("DELETE FROM {term_color} WHERE tid='%d'", $tid);
}
` It might appear that I'm making adding a colour for a given term more complicated than it needs to be, by deleting a record and inserting a new one instead of updating; however by doing it this way I'm keeping on top of enforcing the integrity of the cache, a new aspect to the module which you'll see being introduced in this function: `
function taxonomy_color_add($tid, $color) {

  $count = db_result(db_query('SELECT COUNT(tid) FROM {term_color} WHERE tid=%d', $tid));
  if ($count == 1) {
    // Delete old color before saving the new one.
    taxonomy_color_delete($tid);
  }

  if (db_query("INSERT INTO {term_color} (tid, color) VALUES ('%d', '%s')", $tid, $color)) {
    cache_clear_all("taxonomy_color:$tid", 'cache_tax_color');
    return TRUE;
  }
  else {
    return FALSE;
  }
}

As you can see, when we delete a colour held against a term and add a new one, we clear any record for this term from the cache, where the key is taxonomy_color:tid. The second parameter of the call to cache_clear_all is the name of the table used to store the cached data. Note that I could / should use drupal_write_record here, but I'm going to keep it simple for now.

The Module (so far) in Full

Below, you'll find the module in full, at least up until this point.

/**
 * @file
 * Enables colors to be assigned to taxonomy
 *
 * @author Lukas White <hello@lukaswhite.com>
 */

/**
 * Implementation of hook_perm().
 */
function taxonomy_color_perm() {
  return array('administer taxonomy');
}

/**
 * Implementation of hook_form_FORM_ID_alter().
 */
function taxonomy_color_form_taxonomy_form_term_alter(&$form, &$form_state) {

  $form['submit']['#weight'] = 99;
  $form['delete']['#weight'] = 100;

  $tid = $form['tid']['#value'];
  if (is_numeric($tid)) {
    $color = taxonomy_color_get_term_color($tid);
  } else {
    $color = '000000';
  }

  $form['extras'] = array(
    '#type' => 'fieldset',
    '#title' => t('Extras'),
    '#collapsible' => TRUE,
  );

  $form['extras']['color'] =array(
    '#type' => 'textfield',
    '#title' => t('Color'),
    '#size' =>  6,
    '#default_value' => $color,
    '#description' => t('The Color that represents this term.'),
  );

}

/**
 * Implementation of hook_taxonomy().
 */
function taxonomy_color_taxonomy($op, $type, $form_values = NULL) {
  // We're only interested in term changes.
  if ($type != 'term') {
    return;
  }

  if (isset($form_values['tid'])) {
    // grab the tid  
    $tid = $form_values['tid'];
    switch ($op) {
      case 'insert':
      case 'update':
        // if we're inserting or updating, set the color
        if (!empty($form_values['color'])) {
          taxonomy_color_add($tid, $form_values['color']);
        }        
        break;
      case 'delete':
        // delete the appropriate row in term_color
        taxonomy_color_delete($tid);
        break;
    }
  }
}

/**
 * Helper function for adding a colour to a term
 */
function taxonomy_color_add($tid, $color) {

  $count = db_result(db_query('SELECT COUNT(tid) FROM {term_color} WHERE tid=%d', $tid));
  if ($count == 1) {
    // Delete old color before saving the new one.
    taxonomy_color_delete($tid);
  }

  if (db_query("INSERT INTO {term_color} (tid, color) VALUES ('%d', '%s')", $tid, $color)) {
    cache_clear_all("taxonomy_color:$tid", 'cache_tax_color');
    return TRUE;
  }
  else {
    return FALSE;
  }
}

function taxonomy_color_get_term_color($tid) {
  $result = db_fetch_object(db_query("SELECT color FROM {term_color} WHERE tid = '%d'", $tid));
  if (is_object($result)) {
    return $result->color;
  } else {
    return '';
  }
}

/**
 * Delete the color associated with a given term
 */
function taxonomy_color_delete($tid) {
  return db_query("DELETE FROM {term_color} WHERE tid='%d'", $tid);
}

/**
 *  Implementation of hook_flush_caches().
 */
function taxonomy_color_flush_caches() {
  return array('cache_tax_color');
}

/**
 *  Implementation of hook_views_api().
 */
function taxonomy_color_views_api() {
  return array(
    'api' => 2,
    'path' => drupal_get_path('module', 'taxonomy_color'),
    );
}

/**
 *  Implementation of hook_views_handlers().
 */
function taxonomy_color_views_handlers() {
  return array(
    'info' => array(
      'path' => drupal_get_path('module', 'taxonomy_color'),
      ),
    'handlers' => array(
      'views_handler_field_taxonomy_color' => array(
        'parent' => 'views_handler_field',
        ),
      ),
    );
}

In the next part, I'll complete the module by implementing caching fully, and then adding Views integration.