Taxonomy Colour: Drupal Module Development Tutorial (Part Two)

4th July 2011 | Tags:

In the first part, I looked at cre­at­ing the data­base schema for a sim­ple Dru­pal mod­ule designed to allow you to asso­ciate colours with tax­on­omy terms. In this sec­ond part, I’ll look at the admin­is­tra­tion aspects of the mod­ule. In essence, what we need to do is as follows:

  • Since colour-​coding may only be appro­pri­ate to cer­tain vocab­u­lar­ies (e.g. cat­e­gories) and not oth­ers (tags, per­haps). We need to pro­vide the user with the option to spec­ify which vocab­u­lar­ies are applicable.
  • The user needs to be able to spec­ify a colour when adding or edit­ing a term, where appropriate.
  • We need to pro­vide a mech­a­nism which allows the colour asso­ci­ated with a term to be stored, retrieved, and to be displayed.

Later, I’m going to want to incor­po­rate the colour in views, as well as con­sider some per­for­mance aspects. The next thing, then, is to start cod­ing the mod­ule and this will take place in the PHP file we cre­ated in part one, taxonomy_color.module.

Intro­duc­ing hook_​form_​alter

To allow the option to cat­e­gorise terms on a per-​vocabulary basis, we’ll add a check­box to the edit vocab­u­lary form (which is also used to add a new vocab­u­lary). We achieve this by har­ness­ing the power of hook_form_alter. In order to inter­cept the form and add this check­box, we need to cre­ate a method whose name matches the sig­na­ture hook_form_FORM_ID_alter. The first part of this tuto­r­ial intro­duced hooks, and I intro­duced the idea that the the hook part should be replaced by the ID of the mod­ule, thus: taxonomy_color_form_FORM_ID_alter. To find our form ID, the eas­i­est way is to get the form on screen by nav­i­gat­ing to the rel­e­vant page (in this case, go and edit an exist­ing 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, how­ever, get rid of those dashes and replace them with under­scores, i.e. taxonomy_form_vocabulary — so the method name becomes: taxonomy_color_form_taxonomy_form_vocabulary_alter Quite a mouth­ful, huh? This mech­a­nism for deter­min­ing your func­tion name might seem unwieldy and com­pli­cated, but you soon get the hang of it. Con­sult­ing the doc­u­men­ta­tion /​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 imple­ment­ing the func­tion by adding the form element.

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

Once you’ve added this new ele­ment, the form should look some­thing like the fig­ure below:

The Add /​Edit Vocab­u­lary form, after we’ve added a new check­box (“Use colour-​coding”)

You’ll prob­a­bly notice a minor issue imme­di­ately — the new ele­ment sits below the Save (i.e. the sub­mit) and Delete but­tons. Whilst the form is still fully func­tional, it’ not ideal from a usabil­ity point-​of-​view. In order to fix this we jug­gle the weights, which con­trol the order in which ele­ments appear within a form. Let’s first set the weight — con­fig­ured as an ele­ment of the array that rep­re­sents the ele­ment — to a high num­ber, 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 sub­mit) and Delete but­tons to greater val­ues (the greater the value, the fur­ther down the form the ele­ment will appear), thus:

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

The form should now be re-​ordered, as illus­trated by the fig­ure below:

The amended form, now we’ve jug­gled the weights around

I won’t go into the mechan­ics of the Dru­pal Form API — you may wish to refer to the Form API Quick­start Guide — but we’re basi­cally adding a new ele­ment called color, which is a check­box with the given cap­tion (the t() func­tion — which stands for trans­late — allows us to localise our titles and mes­sages). Now, the other ele­ment which so far I’ve just skimmed over — the default_value ele­ment. In truth this is prob­a­bly eas­ier to think of as an ini­tial value; when adding a new vocab­u­lary it’s a default value but when edit­ing it’s the cur­rent value. But before we can set this value, we need to decide where it’ going to get stored.

Using Vari­ables in Drupal

Cre­at­ing a table to hold these val­ues is prob­a­bly overkill, how­ever Dru­pal pro­vides sim­ple key-​value stor­age via the variable sys­tem, so that’s what we’ll do. What I’m going to do, then, is sim­ply have a vari­able for each vocab­u­lary indi­cat­ing whether to apply a colour scheme or not. Stick­ing with the con­ven­tion of pre­fix­ing every­thing with the mod­ule name to ensure it’s unique, I’ve stumped for taxonomy_​color_​color_​vocab_​vid, where vid is the vocab­u­lary ID. So the value of $default is deter­mined as follows:

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

If we’re edit­ing a vocab­u­lary (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 cur­rent value for the given vocab­u­lary, first append­ing the vid to the key. The sec­ond argu­ment to the func­tion variable_get spec­i­fies the value to return if the key in ques­tion doesn’t have a value — 0 (false) seems a sen­si­ble default. If the vid isn’t set then it– a new vocab­u­lary, so let’ set it to false.

Using hook_​taxonomy

Next up, this value needs to be stored at the appro­pri­ate time — in this case when the form gets sub­mit­ted; how­ever there’s a trick we can use to “hook in” to this part of the process; when a vocab­u­lary 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 func­tion gets called when insert­ing, updat­ing or delet­ing a vocab­u­lary or a term. $op indi­cates the oper­a­tion (insert, update or delete), and the sec­ond argu­ment — $type — indi­cates whether the entity in ques­tion is either a term or vocab­u­lary. 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 inter­ested in vocab­u­lar­ies right now — but we’ll use a switch here because in a moment, we’ll be look­ing at terms. Another switch decides what to do based on the oper­a­tion; set­ting the vari­able we defined pre­vi­ously on an insert or an update, and if we’re delet­ing a vocab­u­lary then let’s keep things tidy by delet­ing the cor­re­spond­ing variable.

Assign­ing Colour to Tax­on­omy Terms

Next up, we need to add the option to spec­ify a colour for a given tax­on­omy term by adding a new field to the term edit form. Again, we achieve this using hook_form_alter, adding a new field­set con­tain­ing a textfield. Ide­ally we’d prob­a­bly want to use JQuery to trans­form this ele­ment into a colour picker, but that’s out­side the scope of this tuto­r­ial. 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 apply­ing the same logic as before, our func­tion name becomes: taxonomy_color_form_taxonomy_form_term_alter By inspect­ing the form we can see that the term ID (tid) is stored in $form['tid']['#value'], so we can check for a colour asso­ci­ated with that term ID, oth­er­wise default­ing to black (hex 000000). This is shown in the snip­pet below.

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

You’ll prob­a­bly notice imme­di­ately that I’ve called a func­tion which I haven’t yet defined; its pur­pose should be self explana­tory. 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, sub­sti­tut­ing the pro­vided term ID (tid) and fetch the result as an object. The db_query func­tion pro­vides an API to our data­base. Note that the table name is wrapped in curly braces; if a table pre­fix was allo­cated upon instal­la­tion, 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 mod­ify the form, we check whether the vocab­u­lary of this term uses colour-​coding; whether edit­ing or adding a brand new term the vocab­u­lary is, by design, already spec­i­fied and is stored in $form['#vocabulary']['vid'] so we use this to check the value of the rel­e­vant variable.

Using the Data­base in Drupal

So, that’s the form mod­i­fied, but we still need to store the colour in the table we cre­ated. Here we’ll go back to our imple­men­ta­tion of hook_taxonomy(), and add a clause to the switch state­ment 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 func­tions to add /​update /​delete a colour for a given term. Delet­ing 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 sec­ond para­me­ter 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 sim­ple for now.

The Mod­ule (so far) in Full

Below, you’ll find the mod­ule 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 com­plete the mod­ule by imple­ment­ing caching fully, and then adding Views integration.

Comments

    Thanks for details.need more clear description.

    10th December 2011
    shawon
    shawon

    Thanks for details description…

    14th December 2011
    feona
    feona

    Thanks for the great write up so far … hopefully you’ll be able to do part 3 some time soon.

    22nd January 2012
    Tim
    Tim

    Of the panoply of website I’ve pored over this has the most veraicty.

    20th October 2012
    Brigid
    Brigid

    Great post! I am just starting out in community management/marketing media and trying to learn how to do it well - resources like this article are incredibly helpful. As our company is based in the US, it?s all a bit new to us. The example above is something that I worry about as well, how to show your own genuine enthusiasm and share the fact that your product is useful in that case.
    http://www.epicresearch.co/products/commodity-tips

    1st November 2012
    MCX tips
    MCX tips

    Really impressed! Whatever are the explanation very open and very clearly the issues?

    1st November 2012
    Commodity Tips
    Commodity Tips

Links and images are allowed, but please note that rel="nofollow" will be automactically appended to any links.