When building a module that requires a custom administration settings page, the system_settings_form() is indispensible. It saves the contents of a form into system variables, and this frees developers from having to write their own #submit callback for a form. This turns out to be a huge timesaver when building forms with simple things like checkboxes and select fields that don’t really need to be validated or transformed.

But typically this function isn’t considered for more complex operations or data types beyond integers and strings. What if you want to save an array to a variable? Or what if you need to transform a string value containing a username into an integer user ID?

Normally, these cases would require a custom #submit callback, but I want to propose an alternative. I’m going to outline an approach to this problem that utilizes the system_settings_form() to save transformed variables to the database. I’ll then follow up with two real-world implementations.

The problem

It makes no sense to save a untransformed value to the variable, because that means you’ll just have to write a getter function and call it each time you want to use the variable for anything meaningful. That takes precious nanoseconds of page load time, and is completely pointless. Instead, you should take the time to do the expensive processing only when the variable is changed. Then, when it comes time to use your variable, you’re ready to go as soon as you call variable_get().

(Plus if you’re slightly OCD like me, especially when it comes to the integrity of your data, it’s irritating to know that there’s ugliness living in your database that you’re going to have to clean up anytime you want to use it.)

But why not just write your own #submit callback that does the same thing? That’s a valid question, and I’ll preface my argument by saying that it’s going to boil down to a matter of taste. It’s my opinion that this is a cleaner approach to the problem. As with all things Drupal, there are multiple other ways to do this. There’s never only one right way with Drupal (but usually several absolutely wrong ways).

It means code portability

With all your logic at the form element scope, you can move that form element into an entirely separate form, and it will work exactly the same. Furthermore, you can add the same form element to another form (for whatever reason) and keep it in your settings form, and reference the same value and validate callbacks.

If you did all of this work inside a #submit callback, you’d end up with duplicated logic at best, and at worst you might be stuck refactoring a form later on if you change the format of the variable.

It means code reusability

Consider the use of an autocomplete on a textfield (which I will demonstrate in my examples below). It’s conceivable that you would have this on more than just one form, and multiple times on the same form. By using the #element_validate and #value_callback approach, you only need to write two functions for an unlimited number of fields.

The method

All you need to do is add #value_callback and #element_validate to your form element.

Start with a settings form that looks something like this:

<?php
function D6MODULE_admin_settings_form() {
  $form['D6MODULE_variable1'] = array(
    '#type' => 'textfield',
    '#value_callback' => 'D6MODULE_admin_variable1_value_callback',
    '#element_validate' => array('D6MODULE_admin_variable1_validate'),
  );
  return system_settings_form($form);
}
?>

The value callback is used to convert the #default_value — which will contain the transformed variable’s value (e.g., an array, or a user ID) — into the format expected on the form:

<?php
function D6MODULE_admin_variable1_value_callback($element, $edit = FALSE) {
  if (func_num_args() == 1) {
    // No value yet, so use #default_value if available.
    if (isset($element['#default_value']) && is_numeric($element['#default_value'])) {
      // Convert internal value to user-friendly value.
      $value = _D6MODULE_admin_variable1_transform_from_internal($element['#default_value']);
      // Return the transformed value to the form.
      return $value;
    }
    // If the #default_value is blank, return empty string here.
    return '';
  }
  // For other calls to the value_callback, return the second parameter.
  return $edit;
}
?>

The element validate callback is used to both validate the data (as needed) and in the event validation passes, to actually do the transformation and save it in the form via form_set_value():

<?php
function D6MODULE_admin_variable1_validate($element, &$form_state) {
  if (!empty($element['#value'])) {
    // Check that the user-friendly value is valid (optional).
    if (!_D6MODULE_admin_variable1_do_validation($element['#value'])) {
      // Failed validation, return form error.
      form_error($element, t('Variable was not valid.'));
    }
    else {
      // If passed validation, convert user-friendly value to internal value.
      $value = _D6MODULE_admin_variable1_transform_to_internal($element['#default_value']);
      // Set the value on the form.
      form_set_value($element, $value, $form_state);
    }
  }
}
?>

Example implementations

Here are some example implementations of this method.

Explode textarea contents into an array

User autocomplete on a textfield but save user ID