When attempting to create a select box with no options, you get the following error message:

warning: Invalid argument supplied for foreach() in D:\php\twins\drupal\includes\form.inc on line 643.

I think the function form_select_options() should also consider the case of empty options, for example with a cast in the foreach:

foreach ((array)$choices as $key => $choice) {
......
}
Support from Acquia helps fund testing for Drupal Acquia logo

Comments

chx’s picture

Status: Active » Reviewed & tested by the community
FileSize
533 bytes

Though the policy is that we do not babysit broken code, for the sake of JS people, and because the cost is negligible, I say, let's do it.

drumm’s picture

Status: Reviewed & tested by the community » Needs work

Shouldn't we default #options to array() in system_elements()?

ricabrantes’s picture

Version: x.y.z » 7.x-dev

Any activity?? moving to new version..

Jaza’s picture

Version: 7.x-dev » 8.x-dev

Moving.

Heine’s picture

From #444860: fix php warning when #options is not set in a select field, the use case for non-set options appears to be to bypass the options checker.

 if (isset($elements['#options']) && isset($elements['#value'])) {

That means our options checker is broken :)

BrockBoland’s picture

FileSize
1.7 KB

Confirmed that this is still a bug in 8.x, if #options is not an array (eg: NULL, 'random string', FALSE).

The attached module demonstrates the problem. Enable "Empty Select Tester" module and navigate to /empty_select_test, and you'll get a warning:

Warning: Invalid argument supplied for foreach() in form_select_options() (line 2664 of /blah/core/includes/form.inc).

BrockBoland’s picture

Status: Needs work » Needs review
FileSize
629 bytes

The attached patch just checks if $choices is an array, and returns an empty string if it is not. This is only slightly different than chx's solution: it bails out of the function before the checks of the element value, since those aren't necessary if $choices isn't valid.

adharris’s picture

Status: Needs review » Needs work
Issue tags: +Needs tests

Patch looks good, but needs tests. Seems the proper way to test would be to add a form to the form_test module which defines a select box with a non-array #options. Rendering the form page should not result in a php warning.

xjm’s picture

+++ b/core/includes/form.incundefined
@@ -2656,6 +2656,10 @@ function form_select_options($element, $choices = NULL) {
+  // If there are no choices, <option> elements cannot be created

Minor note here; the inline comment is also missing a period. So, it would be good to fix this when we add the test. :)

BrockBoland’s picture

Status: Needs work » Needs review
FileSize
2.03 KB
2.65 KB

I think I've got this figured out. This is only the second test I've written, so again: any feedback is very much welcome.

Two patches attached: one with only the new test (which should fail), and the other includes the fix (which should pass tests).

The test case simply requests a form that has a select element with '#options' => NULL. As adharris noted in #8, it doesn't need to do any assertions on the display of that form, because SimpleTest will report the PHP warning if it's thrown.

I tweaked the fix slightly from the one in #7 to check count($choices) instead of is_array($choices), so that it uses the same method as form_process_radios().

Status: Needs review » Needs work

The last submitted patch, 78292-10.test-only.patch, failed testing.

droplet’s picture

+++ b/core/includes/form.incundefined
@@ -2662,6 +2662,10 @@ function form_select_options($element, $choices = NULL) {
+  if (!(count($choices) > 0)) {

it should rewrite to

  if (!isset($choices)) {
    $choices = $element['#options'];
    if (empty($choices) && !is_array($choices)) {
      return '';
    }
  }
BrockBoland’s picture

Status: Needs work » Needs review

#10: 78292-10.fix_.patch queued for re-testing.

BrockBoland’s picture

Status: Needs review » Needs work
FileSize
1.59 KB
2.21 KB

Made a foolish mistake in my last patch: I left in something I had added to an existing test case when I was first trying to figure out how to write the test:

+++ b/core/modules/simpletest/tests/form_test.moduleundefined
@@ -1113,6 +1120,15 @@ function form_test_select($form, &$form_state) {
+  $form['select'] = array(
+    '#type' => 'select',
+    '#title' => t('Select'),
+    '#multiple' => TRUE,
+    '#description' => t('The description appears usually below the item.'),
+    '#options' => array(),
+    '#default_value' => -1,
+  );

This broke the existing test, because earlier in that function, $form['select'] was set with different values. This patch simply removes this section.

BrockBoland’s picture

Status: Needs work » Needs review

Status: Needs review » Needs work

The last submitted patch, 78292-10.test_only.patch, failed testing.

BrockBoland’s picture

Status: Needs work » Needs review

Hurrah! That patch was the test case only, without the fix, and was intended to fail. Switching back to NR for community review.

xjm’s picture

Status: Needs review » Needs work
Issue tags: -Needs tests +Novice

Thanks @BrockBoland! Great to see a working test that exposes the issue. I've a few minor points for cleanup in the patch:

+++ b/core/modules/simpletest/tests/form.testundefined
@@ -618,6 +618,12 @@ class FormElementTestCase extends DrupalWebTestCase {
+  // Verify that an empty select element does not result in a PHP warning.

This needs to be a docblock rather than an inline comment, and follow the standards for that (so "Verifies" rather than "Verify"). Reference: http://drupal.org/node/1354#functions

+++ b/core/modules/simpletest/tests/form.testundefined
@@ -618,6 +618,12 @@ class FormElementTestCase extends DrupalWebTestCase {
+    // No need to check the page output: SimpleTest will report the PHP warning.

I'd change this comment to describe what the line is doing, not what it's not doing. Also, for completeness, I'd actually prefer to add an assertion for the expected markup.

+++ b/core/modules/simpletest/tests/form_test.moduleundefined
@@ -2012,3 +2019,17 @@ function form_test_required_attribute($form, &$form_state) {
+ * Builds a form to test that no PHP warnings are thrown when #options is not an
+ * array for a select element.

This should be one line of 80 characters or less. Reference: http://drupal.org/node/1354#functions

+++ b/core/modules/simpletest/tests/form_test.moduleundefined
@@ -2012,3 +2019,17 @@ function form_test_required_attribute($form, &$form_state) {
\ No newline at end of file

We need a newline at the end of the file here.

Tagging novice to clean up these points and add an assertion to the test. (The additional assertion does not need to fail; it just needs to codify the expected behavior.) Thanks!

BrockBoland’s picture

Note that the two test files in the patch have since moved: http://drupalcode.org/project/drupal.git/commit/789ab2a

BrockBoland’s picture

Status: Needs work » Needs review
FileSize
1.58 KB
2.2 KB

Updated with xjm's feedback and the new path for the form_test module.

I'm waiting to see what the testbot kicks back for the assertion I added to check for the warning:

$this->assertNoText(t('Warning: Invalid argument supplied for foreach()'));

My local dev instance is NOT printing the warning to the page (thought my error_reporting is cranked up), so this assertion never fails. The test still fails, though, because of the PHP warning, but that assertion doesn't fail. This may well be a local configuration issue, though, which is why I'm curious what the bot says.

Status: Needs review » Needs work

The last submitted patch, 78292-20_testonly.patch, failed testing.

xjm’s picture

Status: Needs work » Needs review
xjm’s picture

Status: Needs review » Needs work

Ah, I see. Generally testing for the text of a PHP error like that won't work. What I suggested in #18 was to add an assertion for what should happen, for the content we expect to see on the page when the bug is not present. In this case, I think it's an empty select element that we expect to be rendered, which we can test for with DrupalWebTestCase::assertFieldByXPath().

The idea is that the automated test is not merely checking to make sure the particular error is not there, but also adding the test coverage that was missing, to codify the expected behavior of the element under this circumstance. Like I said, this additional assertion does not need to fail.

The rest of the cleanup looks good. Thanks @BrockBoland!

Traverus’s picture

Assigned: Unassigned » Traverus

I'm here to help!

Traverus’s picture

FileSize
2.25 KB

I'm kind of a XPath newb, lets see if this works.

BrockBoland’s picture

Status: Needs work » Needs review

Changing status for the test bot. I'll manually test it once it clears that.

Status: Needs review » Needs work
Issue tags: -Novice

The last submitted patch, 78292-30_fixed.patch, failed testing.

BrockBoland’s picture

Status: Needs work » Needs review

#25: 78292-30_fixed.patch queued for re-testing.

Status: Needs review » Needs work
Issue tags: +Novice

The last submitted patch, 78292-30_fixed.patch, failed testing.

BrockBoland’s picture

Status: Needs work » Needs review
FileSize
2.19 KB

This is the patch from #25 with a newline at the end of the file.

Status: Needs review » Needs work

The last submitted patch, 78292-30.patch, failed testing.

xjm’s picture

#30 is the complete patch, so we'd expect it to pass if the test were working properly. However, the assertion is failing.

I'd recommend running the test locally and inspecting the verbose output to verify that this element is in fact printed, and then adjusting the xpath from there until the test passes locally. Then, upload both a test-only patch and a complete patch to expose the expected failures and confirm that the patch resolves them.

Additionally, there are a couple things to clean up:

+++ b/core/modules/system/tests/form.testundefined
@@ -620,6 +620,14 @@ class FormElementTestCase extends DrupalWebTestCase {
+   * Verifies that an no empty select elements exist

This isn't quite a complete sentence (and needs a period). I'd suggest:
Tests a select element when #options is not set.

+++ b/core/modules/system/tests/form.testundefined
@@ -620,6 +620,14 @@ class FormElementTestCase extends DrupalWebTestCase {
+    $this->assertFieldByXPath("//option/select[1]", NULL, t('Empty option element found.'));

Let's remove t() from the assertion message. Reference: http://drupal.org/simpletest-tutorial-drupal7#t

thoufek’s picture

Assigned: Traverus » thoufek
Issue tags: +tcdrupal2012

Trying to reproduce locally.

thoufek’s picture

Assigned: thoufek » Unassigned
Status: Needs work » Needs review
FileSize
2.26 KB

I incorporated the feedback from Comment #32:

Changed the function header doc to "Tests a select element when #options is not set."

Test verifies that a select element is present, and that it contains no option element.

xjm’s picture

Status: Needs review » Reviewed & tested by the community

This looks great. Thanks @thoufek!

droplet’s picture

Can we set the $choices thought code ?

if YES, $choices = 1 will passed and give errors ?

if NO, why not use ===

Thanks.

sun’s picture

Status: Reviewed & tested by the community » Needs work
+++ b/core/includes/form.inc
@@ -2662,6 +2662,10 @@ function form_select_options($element, $choices = NULL) {
+  if (!(count($choices) > 0)) {

I don't understand why this isn't just simply empty() ?

Furthermore, the suggestion in #2 makes sense to do in addition to that; i.e., default #options to an empty array in system_element_info().

BrockBoland’s picture

@sun: I stuck with count() to handle a variety of invalid values for (what should be) the $choices array. Setting '#options' => array() in system_element_info(), as suggested in #2, is a good start because it sets the default for #options, but the warning will still be thrown if a module author sets #options to an invalid value in the form API: NULL, TRUE, FALSE, a number or string, etc. NULL and FALSE would be caught by empty(), but actual values would not.

Though, the same is true of count(), so really, this check should be:

if (!is_array($choices) || !(count($choices) > 0)) {
  return '';
}
BrockBoland’s picture

Assigned: Unassigned » BrockBoland

Working on updating this patch for the new location of tests, and to add what I mentioned in the previous comment.

BrockBoland’s picture

Assigned: BrockBoland » Unassigned
Status: Needs work » Needs review
FileSize
3.15 KB

Updated to apply to the new test class and adjusted the condition:

  // If there are no choices, <option> elements cannot be created.
  if (!is_array($choices) || !(count($choices) > 0)) {
    return '';
  }

I also added the default value for #options in system_element_info().

BrockBoland’s picture

Issue tags: +drupaldelphia2012

(just tagging the event)

patrickd’s picture

FileSize
3.15 KB

patch did not apply anylonger, reroll

sun’s picture

Title: warning when creating select box with no options » Select element with empty options triggers PHP warning
FileSize
2.99 KB

Simplified. We only need to check once when the function is initially called from theme_select().

Please note that this can produce invalid HTML markup under certain conditions, since the spec for select says:

  • A select element with a required attribute and without a multiple attribute, and whose size is “1”, must have a child option element.
  • The first child option element of a select element with a required attribute and without a multiple attribute, and whose size is “1”, must have either an empty value attribute, or must have no text content.

That is, because theme_select() unconditionally wraps the output of form_select_options() into a <select></select> element, regardless of whether it is empty.

However, this entire case of empty #options is so extremely bogus in the first place that I don't think it makes any sense to cater for that.

Thus, I think this is RTBC.

patrickd’s picture

Status: Needs review » Reviewed & tested by the community

also fine for me

catch’s picture

Status: Reviewed & tested by the community » Needs review

Does this absolutely need a full page callback to test it?

 
   /**
+   * Tests a select element when #options is not set.
+   */
+  function testEmptySelect() {
+    $this->drupalGet('form-test/empty-select');
+    $this->assertFieldByXPath("//select[1]", NULL, 'Select element found.');
+    $this->assertNoFieldByXPath("//select[1]/option", NULL, 'No option element found.');
+  }

Seems like a drupal_get_form() + drupal_render() would be enough?

kscheirer’s picture

Status: Needs review » Reviewed & tested by the community

That seems to be the pattern all throughout FormTest.php - are you asking that they all change?

catch’s picture

Status: Reviewed & tested by the community » Fixed

They probably should, but not in this patch. Committed/pushed to 8.x.

Status: Fixed » Closed (fixed)
Issue tags: -Novice, -tcdrupal2012, -drupaldelphia2012

Automatically closed -- issue fixed for 2 weeks with no activity.