Documentation location/URL

https://api.drupal.org/api/drupal/developer!topics!forms_api_reference.h...

Problem/Motivation

The 'values' section for #empty_value contains the following text.

Values: Defaults to empty string. Can be anything except NULL.

anything except NULL would mean that #empty_value could also be set to TRUE, an object, or an array.
Setting #empty_value to TRUE when the form element is defined as in the following code would also have a side effect.

$form['selected'] = array(
  '#type' => 'select',
  '#title' => t('Selected'),
  '#options' => array(
    0 => t('No'),
    1 => t('Yes'),
  ),
  '#empty_value' => TRUE,
  '#default_value' => $category['selected'],
  '#description' => t('Set this to <em>Yes</em> if you would like this category to be selected by default.'),
);

With that code, #options would be changed to array(0 => t('No'), 1 => t('- None -')). This happens because form_process_select() contains the following code.

// The empty option is prepended to #options and purposively not merged
// to prevent another option in #options mistakenly using the same value
// as #empty_value.
$empty_option = array(
  $element['#empty_value'] => $element['#empty_option'],
);
$element['#options'] = $empty_option + $element['#options'];

In PHP, settings $array[TRUE] or $array['1'] is like setting $array[1]. This means that the option whose value is 1 will be replaced by the empty option, when the empty option value is TRUE.

The documentation should not say that #empty_value can be any value and make clear that #empty_value can overrides a value given for #options.

[The issue summary have been updated to make more explicit what the issue is.]

Original report

The 'values' section for #empty_value states "Values: Defaults to empty string. Can be anything except NULL." -- this is incorrect, if it is set to the value TRUE then the empty option will overwrite $options[1] preventing the original value in $options[1] from being selected.

Comments

jhodgdon’s picture

Project: Drupal core » Documentation
Version: 7.x-dev »
Component: documentation » API documentation files
Issue tags: +FAPI reference

The Form API reference is in the Documentation project Git repository, so moving to that project.

jhodgdon’s picture

Issue summary: View changes

instructions inadvertently left in description.

apaderno’s picture

Title: Documentation incorrect regarding #empty_value ('anything except NULL' is incorrect) » Fix the documentation for #empty_value
Category: Bug report » Task
Issue summary: View changes
apaderno’s picture

Issue summary: View changes
apaderno’s picture

form_process_select() gives the following description for #empty_value.

#empty_value: (optional) The value for the first default option, which is used to determine whether the user submitted a value or not.

  • If #required is TRUE, this defaults to '' (an empty string).
  • If #required is not TRUE and this value isn't set, then no extra option is added to the select control, leaving the control in a slightly illogical state, because there's no way for the user to select nothing, since all user agents automatically preselect the first available option. But people are used to this being the behavior of select controls. @todo Address the above issue in Drupal 8.
  • If #required is not TRUE and this value is set (most commonly to an empty string), then an extra option (see #empty_option above) representing a "non-selection" is added with this as its value.

It is different from the description given in #empty_value, but it does not say anything about setting #empty_value to TRUE.

Even the code in form_process_select() does not seem to handle #empty_value in a particular way, when it is equal to TRUE.

    // If the element is required and there is no #default_value, then add an
    // empty option that will fail validation, so that the user is required to
    // make a choice. Also, if there's a value for #empty_value or
    // #empty_option, then add an option that represents emptiness.
    if ($required && !isset($element['#default_value']) || isset($element['#empty_value']) || isset($element['#empty_option'])) {
      $element += array(
        '#empty_value' => '',
        '#empty_option' => $required ? t('- Select -') : t('- None -'),
      );

      // The empty option is prepended to #options and purposively not merged
      // to prevent another option in #options mistakenly using the same value
      // as #empty_value.
      $empty_option = array(
        $element['#empty_value'] => $element['#empty_option'],
      );
      $element['#options'] = $empty_option + $element['#options'];
    }
  }
apaderno’s picture

The reason for which #empty_value say it can be any value except NULL is that form_process_select() checks its value with isset($element['#empty_value']); for a NULL value, isset() returns FALSE.

Given that #empty_value is used as array index, saying it can be any value is too broad. It can be any value PHP allows for an array index.

tunic’s picture

I think the problem is in:

 $empty_option = array(
        $element['#empty_value'] => $element['#empty_option'],
      );

Let's say we have this array of options ($element['#options']) like this:

$element['#options'] = [ 
  0 => 'zero', 
  1 => 'one', 
  2 => 'two'
];

If #required is TRUE and #empty_value is TRUE execution will reach that line, setting $empty_option to:

$empty_option = [ 
  TRUE => 'empty option string'
];

TRUE is equivalent to 1, so when execution reaches the line:

$element['#options'] = $empty_option + $element['#options']

... the final value of $element['#options'] will be:

[
  1 => "empty option string",
  0 => "zero",
  2 => "two",
];

The array value with index 1 is overwritten.

But the problem is not #empty_value is TRUE but #empty_value having a value that's already used in the $element['#options'] array.

So I would say that #empty_value can be any valid PHP array index value but should not collide with any already defined index value of $element['#options'] because it will be overwritten.

I have deduced this form the code, I didn't test it manually using a real Drupal site.

In Drupal 10 almost the same code is present. I'm wondering if Form API should check if the #empty_value is already in use and trigger an error, or fail silently. I think the second option will lead to obscure bugs hard to debug.

apaderno’s picture

@tunic Thank you! Your explanation makes clear what the exact issue is. I tried the following code on PHP 7.4; it prints out 100.

$array = [];
$array[TRUE] = 100;
echo $array[1], "\n";

Even replacing the $array[TRUE] = 100; line with $array['1'] = 100; would get the same value printed.

apaderno’s picture

Issue summary: View changes
tunic’s picture

Good! What would be the next steps? I'm not sure where is the code used to generate https://api.drupal.org/api/drupal/developer!topics!forms_api_reference.h.... Or just content in D.O?

Additionally, I was wondering about triggering a warning is #empty_value is already used in #options, but it seems Logger is not available in the class.

But I think it would be great to do somethingf when the problem is detected.

        // The empty option is prepended to #options and purposively not merged
        // to prevent another option in #options mistakenly using the same value
        // as #empty_value. However, do something is this condition is detected.
        if (array_key_exists($element['#empty_value'], $element['#options'])) {
          // Do something.
        }
        
        $empty_option = [$element['#empty_value'] => $element['#empty_option']];        
        $element['#options'] = $empty_option + $element['#options'];
apaderno’s picture

The file used for the Drupal 7 Form API Reference page is the forms_api_reference.html file in this very project repository.

The documentation page for #empty_value is present in Drupal 10 too, even if the documentation page is a different one. (See Select.)
The documentation page should be first changed on Drupal 10, if it needs to be changed, and then the Drupal 7 documentation page should be changed to match the Drupal 10 documentation page (at least for the #empty_value part).

Can be anything except NULL. in the Drupal 7 documentation should at least be removed, or changed.

tunic’s picture