Support for Drupal 7 is ending on 5 January 2025—it’s time to migrate to Drupal 10! Learn about the many benefits of Drupal 10 and find migration tools in our resource center.
Problem
- Specifying a
'#default_value' => 0
in a set of radio button#options
using string keys checks all options.
Example
$form['myradios'] = array(
'#type' => 'radios',
'#options' => array(
0 => '- None -',
'one' => 'one',
'two' => 'two',
'three' => 'three',
),
'#default_value' => 0,
);
Result:
<input id="edit-myradios-0" name="radios" value="0" checked="checked" class="form-radio" type="radio">
<input id="edit-myradios-one" name="radios" value="one" checked="checked" class="form-radio" type="radio">
<input id="edit-myradios-two" name="radios" value="two" checked="checked" class="form-radio" type="radio">
<input id="edit-myradios-three" name="radios" value="three" checked="checked" class="form-radio" type="radio">
Example of steps to reproduce in core:
- Edit any node view
- Add 'Published' filter
- Tick 'Expose ...'
- Select '- Any -' under 'Published status'
- Save filter
- Go to filter form again. 'No' is selected instead of '- Any -'.
Comment | File | Size | Author |
---|---|---|---|
#154 | 1381140-154.patch | 8.72 KB | Lendude |
#154 | interdiff-1381140-152-154.txt | 3.35 KB | Lendude |
Comments
Comment #2
xjmMaybe there is a string vs. int issue going on?
0 == '0'
is true but0 === '0'
is false.Comment #3
JeremyFrench CreditAttribution: JeremyFrench commentedI think I have been caught out by this in the past.
Obviously the work around is to rename the key of your default value to something other than 0.
Comment #4
sunAfter analyzing this further and fixing it, I'm relatively confident that I'm partially duplicating another issue in the queue, but who cares.
Comment #6
Damien Tournoud CreditAttribution: Damien Tournoud commentedPlease keep this issue focused. The change in
_form_validate()
is unrelated (and untested).Comment #7
xjmRe: #6 -- I think the change in
_form_validate()
is necessary for this change to work? It eliminates hundreds of test failures for the original change.Comment #8
sunMerged this one-line fix into #811542: Regression: Required radios throw illegal choice error when none selected, as it fixes the overall #value and #default_value handling of #type 'radios'.
Comment #9
sunRe-opening, since this is too much for #811542: Regression: Required radios throw illegal choice error when none selected
Comment #10
tim.plunkettRerolling after the test files moved. I didn't make any other changes.
Comment #12
tim.plunkettThis needs more tests, the current tests without the patch pass.
Comment #13
chx CreditAttribution: chx commentedThanks for fixing this annoying problem. However, my gut reaction is that anyone patching existing tests in form_test_checkboxes_radios needs to explain himself quite throughly and I do not see that anywhere in the issue. Adding new tests is always fine but why are you changing existing ones? We broke checkboxes enough times that it haunts me six years later so having like a hundred tests in place to keep them working makes me sleep better and changing those tests makes me twitchy.
Comment #14
cweagansFixing tags per http://drupal.org/node/1517250
Comment #15
dcam CreditAttribution: dcam commented#10: drupal-1381140-10.patch queued for re-testing.
Comment #17
dcam CreditAttribution: dcam commentedRerolled #10 after test files moved. No other changes. My first attempt at rerolling a patch.
Comment #19
sunModules can easily work around this bug, so downgrading to normal.
Comment #20
dcam CreditAttribution: dcam commentedThe cause of the failures appears to be that PDO always fetches values as strings. The data type check introduced by the patch causes the test failures when comparing an integer value to the string equivalent that was fetched.
I can think of a couple of ways to work around this, either converting all values to strings or all fetched values to the correct type, but I'm not sure what the recommended approach is. Maybe someone else has a better idea. I'd like to write a patch, but I could use some other opinions.
Comment #21
tanmaykThis is my first patch for core. It worked as expected but not done any changes with form tests.
Comment #23
esoteric1 CreditAttribution: esoteric1 commentedI couldnt apply the last patch. here is a patch containing the modification from #21 and also a a hook_menu and form builder for the test. I didnt have time to write the test part.
Comment #24
esoteric1 CreditAttribution: esoteric1 commentedNoticed a small coding standard violation. fixed it.
Comment #25
babruix CreditAttribution: babruix commentedFixed patch and added test part.
Comment #27
babruix CreditAttribution: babruix commentedFor some reason test OptionsWidgetsTest.php fails, however on local it passed tests. Any ideas?
Comment #28
babruix CreditAttribution: babruix commentedComment #29
babruix CreditAttribution: babruix commented#25: drupal8.form-radio-default-test-1381140-25.patch queued for re-testing.
Comment #31
babruix CreditAttribution: babruix commentedTrying another patch, removed condition
Comment #33
Barnettech CreditAttribution: Barnettech commentedFor your convenience, here is where the patch is failing: http://api.drupal.org/api/drupal/core%21modules%21system%21lib%21Drupal%... on line 98 according to the test bot
Comment #34
chx CreditAttribution: chx commentedHere is what you need to do in order to fix this.
Consider everything that is valid as an array index. Consider what it will become during POST. Consider what is needed to match the two together. Your inspiration for a solid test is core/modules/system/lib/Drupal/system/Tests/Form/CheckboxTest.php :
Comment #35
webmozart CreditAttribution: webmozart commentedWe were hit by this problem in the Symfony2 Form component too. Our solution lies in the classes
ChoiceList
andSimpleChoiceList
.Basically, you have to convert any choice value that is entering your choice field logic (this applies for radio fields, checkboxes and drop downs) from either the application (through
#default_value
) or the browser (through a form submission) to a valid array key and then use strict comparison to compare it with the array keys in your#options
array. We use the following function for choice value conversion:Comment #36
babruix CreditAttribution: babruix commentedLooks problem was in WebTestBase->assertFieldChecked() method,
it checked
and should check
Comment #37
chx CreditAttribution: chx commentedComment #39
babruix CreditAttribution: babruix commentedComment #41
babruix CreditAttribution: babruix commentedOk, added fix in form.inc.
Similar issue described in Default check values in tableselect aren't set properly for a 0-based array so probably will combine these two patches into one.
Comment #42
star-szrThanks for moving this forward @babruix! Found some tweaks while reviewing the patch in #41.
This change doesn't seem right - I think WebTestBase.php should be left as is.
This will need a more complete docblock per https://drupal.org/node/1354#forms.
Comment #43
LendudeA similair issue to the one here (and caused by the same line of code), is that currently if you have an #options array containing the key 0, it will always be checked='checked' if the #default_value is a string.
will result in a set of buttons with both 'bla' and 0 checked.
Using === will solve this, but will lead to other issues because of PHP automatically casting numeric keys to (int) and all submitted values being strings.
So this will lead to a rendered set of buttons with none selected when using ===.
(An example of this behaviour can be seen in the default node view included in views in the D8 core, with the Published filter handler.)
My current D7 solution is to cast both #value and #return_value to strings in the comparisson
Does the trick for me (so far).
Len
Comment #44
heddnComment #45
ceardach CreditAttribution: ceardach commentedUpdated patch so it applies cleanly to HEAD
Comment #46
heddnComment #48
IRuslan CreditAttribution: IRuslan as a volunteer and at DrupalJedi commentedUnlike @sun in #19, I think it's a pretty important thing to be fixed because even Drupal core affected by this problem (see #43, for D7 Views is affected correspondingly).
The problem still is not fixed since D7, and most probably from D6 versions.
Even more, in addition to case in the issue description, next case also will lead to error (both elements will have checked attribute, and browser will highligth the last one):
From my point of view solution in all previous patches fixes only one usage of that problem, meanwhile all usages of 'radio' element affected (it's at least 'radios' and table select and all contributed modules which use 'radio' directly).
Most important for me that during a development of contributed modules it's not evident to handle such case. As a result, new silent errors with potential data loss will be introduced into projects.
That's why I propose to fix it directly in the rendering of the 'radio' element.
Patch in the attachment.
Comment #50
IRuslan CreditAttribution: IRuslan as a volunteer and at DrupalJedi commentedActually the failed patch revealed another defect of Views module.
As we could see in DisplayPluginBase->buildOptionsForm():
$options = array(FALSE => $this->t('None'), 'custom_url' => $this->t('Custom URL'));
But it's incorrect to use FALSE as an array key, it's always will be converted to int 0.
That's why it didn't match to an empty default value.
It does not introduce bug itself, but logically it's incorrect to have key 0, and except it to be default in case of FALSE in #default_value.
I've rechecked similar places within Views module(i.e. Drupal\views\Plugin\views\style\Table->buildOptionsForm()) and here we see:
'#options' => array('' => $this->t('None'))
And it's working well.
Comment #52
IRuslan CreditAttribution: IRuslan as a volunteer and at DrupalJedi commentedComment #54
IRuslan CreditAttribution: IRuslan as a volunteer and at DrupalJedi commentedComment #55
IRuslan CreditAttribution: IRuslan as a volunteer and at DrupalJedi commentedComment #56
NickDickinsonWildeLooks good to me
Comment #57
LendudeGreat to see somebody taking another look at this! Nice work @IRuslan.
Are we ok with ending up with an id looking like that (I'm not really)? Not sure why this change has influence on the id being generated, but that looks ugly to me.
This still needs tests, for starters see #34 and other cases spread throughout this issue.
Comment #58
IRuslan CreditAttribution: IRuslan as a volunteer and at DrupalJedi commented@Lendude, we changed the option key for radios element, it leads to changed HTML id (it's based on radios element name + option key).
Yep, tests are required for this changes, I'll try to add them a bit later.
Comment #59
IRuslan CreditAttribution: IRuslan as a volunteer and at DrupalJedi commentedAdded tests.
Comment #60
LendudeLooking good.
Added another test scenario that fails without the patch. And added test-only patch to illustrate the problem.
Comment #62
swentel CreditAttribution: swentel commentedTwo nitpicks.
over 80 chars
same here
Comment #63
LendudeFixed nitpicks and in the case of 2. turned it into an actual English sentence.
Comment #64
cilefen CreditAttribution: cilefen commentedComment #65
gnugetTwo small nitpicks.
Here we have an extra new line when the second if is closed, but we haven't it in the first nor in the third one.
missing comma
Comment #66
Lendudefixed nitpicks
Comment #68
LendudeTagging for VDC because it blocks #2459289: Boolean default values are not saved from working properly.
Comment #69
LendudeRan into some more fun scenarios in #2459289: Boolean default values are not saved (boolean default values) that were still failing with the version of the patch in #66.
Attempt at a fix and more tests.
Comment #71
LendudeOk, so FALSE is a reserved 'empty' value, lets just cast TRUE to 1 then.
Comment #73
blazey CreditAttribution: blazey as a volunteer commentedPatch from #71 works for me. Attaching unit test covering
Drupal\Core\Render\Element\Radio::preRenderRadio
.Comment #75
xjmApparently it no longer blocks #2459289: Boolean default values are not saved because that issue was fixed?
Comment #76
Lendude@xjm well I just wrote the tests for #2459289: Boolean default values are not saved to circumvent this issue. This issue still stops you from properly seeing which value you have selected in a boolean filter. And this issue caused the TEST ONLY patch to pass where it should have failed (see #28 there).
The Views UI is the only place I know in core that is actually effected by this issue, hence the VDC tag.
Comment #77
blazey CreditAttribution: blazey as a volunteer commentedReadded missing form class.
This issue is still affecting Views. To reproduce:
Comment #78
blazey CreditAttribution: blazey as a volunteer commentedComment #79
Lendude@blazey thanks for the extra test coverage and added your steps to the issue summary.
I think this looks great, but I wrote a bit too much of it to RTBC this.
Comment #80
lomasr CreditAttribution: lomasr at gai Technologies Pvt Ltd for gai Technologies Pvt Ltd commentedReproduced the issue on my Local . Please see screen shot . '-Any-' & 'No' both are checked.
Comment #81
lomasr CreditAttribution: lomasr at gai Technologies Pvt Ltd for gai Technologies Pvt Ltd commentedApplied the patch #77 . Now '-Any-' is only checked. But getting error 'You must select a value unless this is an non-required exposed filter. ' . Please see the screenshot.
Comment #82
blazey CreditAttribution: blazey as a volunteer commentedThanks for screenshots. It's not a bug, it's a feature ;). 'Any' cannot be chosen as a default value for required filter. Deselect 'Required' and the error will go away.
Comment #83
lomasr CreditAttribution: lomasr at gai Technologies Pvt Ltd for gai Technologies Pvt Ltd commentedYou are right .I didn't notice it. #77 works
Comment #84
suresh_ewall CreditAttribution: suresh_ewall as a volunteer and commentedHi,
We have applied the patch comment#77. We didn't see it will work. We also facing an same error.
Comment #85
dan2k3k4 CreditAttribution: dan2k3k4 at Amazee Labs commentedThanks @blazey, I've read through the code first, and it makes sense.
Tested with simplytest with the steps to reproduce and seems to work fine.
You can't select '- Any -' for a required value but it works fine if you deselect the required checkbox for the value of the exposed filter.
Comment #86
tstoecklerThere is a double space in this line.
Leaving RTBC under the assumption that can be fixed on commit.
Comment #87
dan2k3k4 CreditAttribution: dan2k3k4 at Amazee Labs commentedAh good catch @tstoeckler, I've updated the patch for you @blazey :)
Comment #88
tstoecklerLooks good, thanks!
Comment #89
jp.stacey CreditAttribution: jp.stacey at Magnetic Phield commentedFixing tags
Comment #90
cilefen CreditAttribution: cilefen commentedComment #92
catchThanks for getting this to RTBC! Couple of things though:
This is the same logic repeated twice, we could add a protected static helper method and do something like
Similar to #1381140-35: A #default_value = 0 for #type radios checks all radios.
This isn't necessary any more, can just remove the @file (and it'll fail coding standards automated checks).
Why does this have to change? Could it break contrib/custom code doing similar?
If it's not necessary here, we could make the change in a follow-up.
Comment #93
tameeshb CreditAttribution: tameeshb at Google Summer of Code commentedChanges from #92
Comment #94
tameeshb CreditAttribution: tameeshb at Google Summer of Code commentedComment #96
tameeshb CreditAttribution: tameeshb at Google Summer of Code commentedComment #97
tameeshb CreditAttribution: tameeshb at Google Summer of Code commentedComment #98
catchThis will need to add the castValue() method, it doesn't exist yet I was just suggesting one.
Comment #100
himanshu-dixit CreditAttribution: himanshu-dixit as a volunteer commentedHere is the new patch,
Comment #102
himanshu-dixit CreditAttribution: himanshu-dixit as a volunteer commentedOops forgot to change $index to $value .
Comment #103
himanshu-dixit CreditAttribution: himanshu-dixit as a volunteer commentedThis should pass tests. But i wonder if i have documented the helper function right?
Comment #104
catchIt converts both (int) 0 and FALSE to '0' so it should be @param (mixed) $value I think.
Similarly the function description should probably be something like "Ensures $element['#default_value'] and $element['#value'] are consistently typed."?
Comment #105
himanshu-dixit CreditAttribution: himanshu-dixit as a volunteer commentedHere is the new patch.
Comment #107
himanshu-dixit CreditAttribution: himanshu-dixit as a volunteer commentedComment #109
himanshu-dixit CreditAttribution: himanshu-dixit as a volunteer commentedOkay, I guess we can use filter_var to check if $value is bool .
Comment #112
himanshu-dixit CreditAttribution: himanshu-dixit as a volunteer commentedThis should definitely work. Also changed the documentation to @return mixed since we are returning mixed values from castValue(). Here is the new patch though i am still not sure about the description of @param and @return . Any suggestions ?
Comment #114
himanshu-dixit CreditAttribution: himanshu-dixit as a volunteer commentedOk, removed the test from DisplayTest.php . I think it's better to create a follow-up for the change in #50 . @catch, Can you review this ? This time i am sure this would pass the tests :)
Comment #115
LendudeCouple of nitpicks:
This is not what the function does. 'Casts passed boolean and integer values to strings'
'A value to cast.'
casted => cast
{ needs to be on the same line as the function declaration
Comment #116
tameeshb CreditAttribution: tameeshb at Google Summer of Code commentedMade changes from #115
Comment #117
LendudeAlmost there I think!
strings typed? 'to strings' or 'to string type' I'd say.
Comment #118
himanshu-dixit CreditAttribution: himanshu-dixit as a volunteer commentedFixed #117 in this patch.
Comment #119
LendudeAll feedback has been addressed, back to RTBC
Comment #121
tameeshb CreditAttribution: tameeshb at Google Summer of Code commentedarray() -> []
Comment #123
tameeshb CreditAttribution: tameeshb at Google Summer of Code commentedComment #124
Pavan B S CreditAttribution: Pavan B S at Valuebound commentedRerolled the patch, please review.
Comment #125
Lendude@Pavan B S you included all sorts of things from a different patch...looks like the states javascript test....
Comment #127
LendudeRerolled the reroll to not include unrelated stuff
Comment #128
mkalkbrennerI solved a related issue slightly different for us in #2712131: D8: Views: UI: -Any- option for non-required boolean yes/no filter won't stick.
One of the issues should be declared as duplicate and include the other use-case, too.
Comment #129
mkalkbrennerI added the use-case from #2712131: D8: Views: UI: -Any- option for non-required boolean yes/no filter won't stick to the tests to see if the patch here solves that as well.
Comment #130
sk33lz CreditAttribution: sk33lz at Zivtech commentedThe patch in #129 applies cleanly to 8.4.x and fixes the issue of selecting "-Any-" then saving the form to find it set to "No" when editing again. The "-Any-" setting stays selected when editing the filter again after applying #129. This allows the exposed filter can be used to view all content, published content, or unpublished content.
This looks RTBC after manual tests.
Comment #131
HongPong CreditAttribution: HongPong as a volunteer and at kor group commentedConfirming #129 fixed my issue on 8.3.2 as well. RTBC I would think, let's please get this onto the 8.3 series as well as 8.4 as it is a serious issue.
Comment #132
alexpottSo this doesn't actually do what it says - it casts bools and ints that can be interpreted as bools - ie. 0 or 1 to string afaics.
It would be great to see other integers tested and all integers as strings tested.
Also if I simply just cast both to a string and do the comparison the tests pass... so either the test is not doing what we think it is or a simpler fix is possible.
Comment #133
xjmInline comments for the renamed
castValue()
would also be ducky because it certainly raised my eyebrows. For example, as I noted to the committers, the (int) cast is to ensure it's not 'octopus' and the (string) cast is to ensure it's '0' or '1'. I guess? But let's explicitly document and explain the "why" of each bit of that line.Comment #134
xjmMaybe it's about NULL rather than 'octopus', which is less exciting. (But then what about 'octopus'?) In any case it clearly needs documentation. :)
Comment #136
Lendude#132.1 removed method, see #132.3
#132.2 added some ints as strings and other ints then 0 and 1
#132.3 Lets make it simple then, unless somebody can come up with a test that breaks it (I couldn't). Also nice to see that I had the same thought back in #43 (4 years ago....), but I never rolled it into a patch, silly me.
Comment #137
Manuel Garcia CreditAttribution: Manuel Garcia as a volunteer and at Appnovation commentedI believe this is blocking #2864610: Convert ExposedFormUITest in views_ui module to BrowserTestBase =)
Comment #139
bapi_22 CreditAttribution: bapi_22 at Cybage Software Pvt Ltd. commentedSince the Options keys are coveted to string after process form elements, So we have to cast the $element['#value'] to string before compare with $element['#return_value']. That will solve the problem.
Comment #140
bapi_22 CreditAttribution: bapi_22 at Cybage Software Pvt Ltd. commentedComment #141
bapi_22 CreditAttribution: bapi_22 at Cybage Software Pvt Ltd. commentedComment #142
Lendude@bapi_22 please try to continue work on existing patches and not just try to start from scratch and remove all the test coverage we have already added. As you can see in #136, that alone won't be sufficient, there are special conditions to consider.
We need to account for people setting the default to an array, which is not valid for a radio button, but people do it anyway, see #2900911: \Drupal\workflows\Form\WorkflowTransitionAddForm::form sets a default value for radios as an empty array.
Also we need to account for people setting keys to FALSE. (as done in \Drupal\views\Plugin\views\display\DisplayPluginBase, will open a follow up for that too)
My solution would be to just say that empty() == empty(), that should clear up most weird configurations.
Added test coverage for these two scenarios.
Comment #144
andypost@Lendude Thanx for working on that! Btw why not just compare values instead of casting them to strings?
I mean
$element['#value'] === $element['#return_value'])
is that intentional?
Comment #145
Lendude@andypost whoops little debugging code left in
We need the casting to make sure string to int comparisons go right, since array keys will always be ints and submitted values will always be strings
Comment #146
andypostLooks great, I think better to extend comment about "submitted values will always be strings"
Comment #147
Lendude@andypost so something like this maybe?
Bug fix so eligible for 8.4.x
Comment #148
andypostIMO perfect!
Comment #150
dcam CreditAttribution: dcam commentedThat failure looks unrelated.
I'd love to see the first core patch I ever worked on finally get committed!
Comment #152
Lendudereroll needed after #2867154: Form: Convert system functional tests to phpunit landed
Comment #154
LendudeThis converts \Drupal\Tests\system\Functional\Form\ElementTest::testRadiosChecked to a proper BrowserTest, that should clear up the fails
Comment #155
andypostThanx a for reroll
Comment #158
catchCommitted/pushed to 8.5.x and cherry-picked to 8.4.x. Thanks!