Drupal 6: Updating nodes when changing CCK field allowed value list using a submit handler

In this article I'll address an issue that has surfaced on every recent project I've been working on: how to update nodes when a CCK field's allowed values list changes. For my projects, I implemented a variety of batch processes and drush scripts to solve this problem. But for this article, I decided to implement a form submit handler solution to automatically update nodes when the CCK field settings page is submitted. Please be careful when implementing code like this, it has potential to update a lot of nodes, and hit memory limitation issues depending on how many nodes need to be updated. As I mentioned before, I implemented a combination of batch APIs and drush scripts to process production environments; I implemented this code in a development environment. Anyway..

I created a new content type "band", and added a CCK text/select field (genre) with an allowed values list:

Allowed value list before

I used Devel's auto-generate content functionality to create 20 nodes (/admin/generate/content), and then added a simple view to show the node's title and genre:

view of nodes before

Now that I had some working test data, I added some code to a custom module to modify the CCK field property settings page, and add a submit handler update the nodes:

<?php

// define module constants
define('CCK_FIELD_PROCESS', 'field_genre');
define('CCK_TYPE_PROCESS', 'band');

/**
 * Implements hook_form_alter()
 */
function helper_form_alter(&$form, &$form_state, $form_id) {

  // check for CCK form, CCK type, and CCK field (see above constants)
  if ($form_id == 'content_field_edit_form' && $form['type_name']['#value'] == CCK_TYPE_PROCESS && $form['field_name']['#value'] == CCK_FIELD_PROCESS) {

    // get original allowed values
    $original_allowed_values_string = $form['field']['allowed_values_fieldset']['allowed_values']['#default_value'];

    // explode values, get array of values
    $exploded = explode("\n", $original_allowed_values_string);
    $original_allowed_values_array = array();
    foreach ($exploded as $data) {

      // explode data on "|"
      list($key, $value) = $exploded2 = explode('|', $data);
      $original_allowed_values_array[$key] = $value;

    }

    // ensure data exists
    if (empty($original_allowed_values_array)) {
      return;
    }

    // store original values in form state storage
    $form_state['storage']['original_allowed_values'] = $original_allowed_values_array;

    // store other information in form state
    $form_state['storage']['cck_type'] = CCK_TYPE_PROCESS;
    $form_state['storage']['cck_field'] = CCK_FIELD_PROCESS;

    // add submit handler
    $form['#submit'][] = '_helper_form_alter_content_field_edit_form_submit';

  }

}

/**
 * Implements custom form submit handler
 */
function _helper_form_alter_content_field_edit_form_submit($form, &$form_state) {

  // get new allowed values list
  $new_allowed_values_string = $form_state['values']['allowed_values'];

  // ensure data exists
  if (empty($new_allowed_values_string)) {
    return;
  }

  // explode values, get array of values
  $exploded = explode("\n", $new_allowed_values_string);
  $new_allowed_values_array = array();
  foreach ($exploded as $data) {

    // explode data on "|"
    list($key, $value) = $exploded2 = explode('|', $data);
    $new_allowed_values_array[$key] = $value;

  }

  // ensure data exists
  if (empty($new_allowed_values_array)) {
    return;
  }

  // NOTE: the next few lines gets the key values from the allowed values
  // and checks to see which are different.
  // This code assumes any change to the keys (including changing the order) will update the nodes.
  // Update to your heart's content
  $new_keys = array_keys($new_allowed_values_array);
  $old_keys = array_keys($form_state['storage']['original_allowed_values']);

  $key_diffs = array();
  foreach ($old_keys as $key => $value) {

    if ($new_keys[$key] != $value) {
      $key_diffs[$value] = $new_keys[$key];
    }

  }

  // ensure data has changed
  if (empty($key_diffs)) {
    unset($form_state['storage']);
    return;
  }

  // get data from form storage
  $cck_field = $form_state['storage']['cck_field'];
  $cck_type = $form_state['storage']['cck_type'];

  // loop through key diffs
  foreach ($key_diffs as $old => $new) {

    // get a list of node ids that need to be updated
    // NOTE: this sql assumes the field is NOT shared
    $sql = "
      select n.nid
      from {node} n
      join {content_type_%s} b on b.nid = n.nid and b.vid = n.vid
      where n.type = '%s' and b.%s_value = '%s'
    ";
    $resource = db_query($sql, $cck_type, $cck_type, $cck_field, $old);
    $node_ids = array();
    while ($row = db_fetch_object($resource)) {
      $node_ids[] = $row->nid;
    }

    // ensure results exist
    if (empty($node_ids)) {
      continue;
    }

    // loop through node ids
    // NOTE: got a lot of nodes? you might need a batch API instead
    foreach ($node_ids as $nid) {

      // load node
      $node = node_load($nid, NULL, TRUE);

      // ensure node loaded
      if (!is_object($node)) {
        continue;
      }

      // update node properties
      $node->{$cck_field}[0]['value'] = $new;
      $node->revision = TRUE;
      node_save($node);

      // remove cck cache for node
      db_query("DELETE FROM {cache_content} WHERE cid LIKE 'content:%d%'", $node->nid);

    }

  }

  // remove form storage
  unset($form_state['storage']);

}
?>

To test this code, I went back to the CCK field settings page (example: /admin/content/node-type/band/fields/field_genre), updated the allowed value list, and saved.

updated allowed value list

The form submit handler is triggered and the nodes are updated. Refreshing my view shows the updated genre fields on my test nodes:

Updated node list