Drupal code: lines 1415 to 1420 of form.inc. form_select_options recursively calls within a foreach. PHP doesn't do this well and iterators should be used because it appears php stores the foreach index in foreach's copy of the array itself and not in the frame. (grrr!)

foreach ($choices as $key => $choice) {
if (is_array($choice)) {
$options .= '<optgroup label="'. $key .'">';
$options .= form_select_options($element, $choice);
$options .= '</optgroup>';
}

My chair (34) will not be in the children of My cube optgroup unless the array is ksorted first.
Array
(
[26] => My cube
[children of 'My cube'] => Array
(
[25] => my computer
[children of 'my computer'] => Array
(
[29] => dust bunnies
)
[34] => my chair <---------- note parent is "children of 'My cube'"
)

[28] => kitchen
)

Produces:

<select name="parent_node" class="form-select" id="edit-parent-node">
<option value="26">My cube</option>
<optgroup label="children of 'My cube'">
<option value="25">my computer</option>
</optgroup>
<optgroup label="children of 'my computer'">
<option value="29">dust bunnies</option>
</optgroup>
<option value="34">my chair</option> <-------- bad
<option value="28">kitchen</option>
</select>

Should be:

<select name="parent_node" class="form-select" id="edit-parent-node">
<option value="26">My cube</option>
<optgroup label="children of 'My cube'">
<option value="25">my computer</option>
<option value="34">my chair</option> <---------- good
</optgroup>
<optgroup label="children of 'my computer'">
<option value="29">dust bunnies</option>
</optgroup>
<option value="28">kitchen</option>
</select>

Comments

lewisvance’s picture

Status: Active » Closed (works as designed)

Recursion within a foreach loop is safe as long as references aren't used. In this case foreach is creating a copy of $choice and the recursive call is creating it's own copy of $choice. Maybe not the most efficient thing in the world but it works.

form_select_options is designed to render the items in the order they appear rather than by key value. It's actually creating the markup you would expect as you can see in this example:

$options = array(
  'Level 0 Item 0',
  'Level 0 Item 0 Children' => array(
    'Level 1 Item 0',
    'Level 1 Item 0 Children' => array(
      'Level 2 Item 0',
    ),
    'Level 1 Item 1',
  ),
  'Level 0 Item 1',
);

$form = array(
  'test' => array(
    '#type' => 'select',
    '#title' => 'title',
    '#options' => $options,
  ),
);

print htmlspecialchars(drupal_render($form));
<div class="form-item"> 
<label>title: </label> 
<select name="" class="form-select" id="" >
  <option value="0">Level 0 Item 0</option>
  <optgroup label="Level 0 Item 0 Children">
    <option value="0">Level 1 Item 0</option>
    <optgroup label="Level 1 Item 0 Children">
      <option value="0">Level 2 Item 0</option>
    </optgroup>
    <option value="1">Level 1 Item 1</option>
  </optgroup>
  <option value="1">Level 0 Item 1</option>
</select> 
</div>

Unfortunately the html spec doesn't allow nested optgroups and the browers just close the first optgroup when they hit a nested one. That's why you end up with the markup in your example.