Overriding Drupal 6 Automatic Select Element Validation

29th March 2011 - 9 minutes read time

Whilst creating a large and complex form in Drupal 6 recently I hit upon a problem that took me a couple of hours to solve so I am posting the solution here in case anyone else gets similarly stuck. I am also writing this down so that I can remember the strategy in case I have to do the same thing again.

What I was doing was creating an event booking form. The form allowed users to select from a number of times slots in a select list, but each time had a limited number of bookings. When the user submitted the form and all allocated slots for their selected time were fully booked they were shown a message asking them to select a different time slot. The original time was removed from the select list so that the user didn't try to select it again. This was fine and it all worked as expected but when this happened I also got the following error message from Drupal.

An illegal choice has been detected. Please contact the site administrator.

This error message is produced by the internal form validation (via the function _form_validate()) that Drupal does to make sure that the item you post for a select element matches one of the items in the list. When you allow the user to select an item and then remove it when the form is recreated after the post request you will get this message as the post value doesn't exist in the select form. This is a security measure to prevent users from hacking the HTML and adding their own items to a select list. Drupal double checks that the element entered matches one of the existing elements in the list and will fail the validation if it doesn't.

After trying various different strategies to get rid of this message in this instance I eventually hit upon the idea of creating the form as normal and temporarily adding the posted value into the forms select list. A form theme hook was then used to scan for and delete the select item that had been artificially added. This allowed me to bypass (at least to a certain extent) the internal Drupal form validation and to include my own form validation to make sure that no fully booked slots were allowed to be booked again.

Rather than copy code from a client project I have created a little example module that does what I have detailed above so that I can describe each step. I have used random numbers to generate the select element, which gives the same effect as using real data. The first step is to create a module folder called select_validate along with the .info and .module files. Here is the contents of the .info file.

; $Id:$
name = Select Validate
description = "Select validate"
core = 6.x

The module file is started by creating a menu hook (with the associated page callback) and a theme hook, which we will use later. This code won't run just yet as we haven't created the select_validate_select_form() definition.

<?php

/**
 * Implementation of hook_menu
 *
 * @return array An array of menu items.
 */
function select_validate_menu() {
  $items = array();
  
  $items['select_validate'] = array(
    'page callback' => 'select_validate_print_form',
    'title' => 'Menu Example',
    'access callback' => TRUE,
  );
  
  return $items;
}

/**
 * Callback function for the select_validate page.
 *
 * @return string The contents of the page.
 */
function select_validate_print_form() {
  $output = '';
  $output .= drupal_get_form('select_validate_select_form');
  return $output;
}

/**
 * Implementation of hook_theme.
 *
 * @return array An associative array of theme hook information.
 */
function select_validate_theme() {
    return array(
        'select_validate_select_form' => array(
            'arguments' => array('form' => NULL),
        ),
    );
}

The next step is to create the form definition function. This function will create a simple form with a single select element filled with 5 random numbers. The important part here is around line 23, where I have added a check for previously posted values within this select statement. If any exist here that don't exist in the select list then they are added into the $random_list array of options with the label of "DELETE". We can then use this string when theming the form to delete this value.

/**
 * Generates the select validate form.
 * 
 * @param array $form_state The form state array.
 *
 * @return array An associative array of form items.
 */
function select_validate_select_form(&$form_state) {
    $form = array();

    $form['#method'] = 'post';
    $form['#validate'][] = 'select_validate_select_form_validate';
    $form['#submit'][] = 'select_validate_select_form_submit';

    $random_list = array('' => '--');

    for ($i = 0; $i < 5; ++$i) {
        $rand = rand();
        $random_list[$rand] = $rand;
    }

    // Check for previously posted values.
    if (isset($form_state['post']['randselect']) && !in_array($form_state['post']['randselect'], array_keys($random_list))) {
         $random_list[$form_state['post']['randselect']] = 'DELETE';
    }

    $form['randselect'] = array(
        '#type' => 'select',
        '#title' => t('Please select'),
        '#required' => TRUE,
        '#options' => $random_list,
    );

    $form['submit'] = array(
        '#type' => 'submit',
        '#value' => t('Go'),
    );

    return $form;
}

The next step is to create a theme function for our form. Because we have set up the theme registry with the same ID (or function name) as the form we can just include a function called theme_select_validate_select_form() and it will be used automatically. The theme for the form can be hard coded within the form definition by adding the following line of code to the select_validate_select_form() function.

$form['#theme'] = 'select_validate_select_form';

The theme function is only used to scan the randselect select element for an option with the label of "DELETE". If it is found then the item is removed before sending the form on to be rendered via a drupal_render() call.

/**
 * Theme the select validate form.
 *
 * @param array $form An associative array of form items.
 *
 * @return string The rendered form.
 */
function theme_select_validate_select_form($form) {
    foreach ($form['randselect']['#options'] as $id => $option) {
        if ($option == 'DELETE') {
            unset($form['randselect']['#options'][$id]);
        }
    }

    $output = drupal_render($form);
    return $output;
}

In order for the above code to be run fully the form must fail validation. The two functions below are tied into the validate and submit calls for the form, but in this case the validate function will always throw an error. It was only needed to demonstrate that the above code worked and to allow other people to adapt this code to their needs, so nothing else is needed in this instance.

/**
 * Validation function for the select validate form.
 *
 * @param array $form An associative array of form elements.
 * @param array $form_state An associative array of the form state.
 */
function select_validate_select_form_validate($form, &$form_state) {
    
    form_set_error('randselect', t('Please select another option.'));
}

/**
 * Submit function for the select validate form.
 *
 * @param array $form An associative array of form elements.
 * @param array $form_state An associative array of the form state.
 */
function select_validate_select_form_submit($form, &$form_state) {
    // save data
}

One final note is that although it is possible to do this, you should only use it when you really have to. The internal validation functions are there fore a very good reason and overriding them like this without being very sure of your own validation functions can be painful (or at least dangerous). I deliberately didn't look at hacking core as the actions of the internal _form_validate() function are useful for stopping rouge items being added to select forms.

Comments

Permalink

Can anybody tell me how to highlight the textfield in drupal after unsuccessful validation?

 

Thanks in advance

Anonymous (Wed, 02/08/2012 - 05:42)

Add new comment

The content of this field is kept private and will not be shown publicly.