Skip to content

Resolve CheckboxElement's value and checked state on read - #208

Open
jrauh01 wants to merge 2 commits into
mainfrom
fix/checkbox-attribute-order
Open

jrauh01 wants to merge 2 commits into
mainfrom
fix/checkbox-attribute-order

Conversation

@jrauh01

@jrauh01 jrauh01 commented Aug 3, 2026

Copy link
Copy Markdown
Contributor

CheckboxElement derived two things eagerly in setValue(): it resolved a boolean value into a string, and it computed the checked flag. Both depend on checkedValue/uncheckedValue, which may not be set yet. Attributes::set() invokes setter callbacks immediately, in the order the attributes are given. So whichever attribute came first won:

// renders checked
new CheckboxElement('test', ['checkedValue' => 'a', 'uncheckedValue' => 'b', 'value' => 'a']);

// renders unchecked
new CheckboxElement('test', ['value' => 'a', 'checkedValue' => 'a', 'uncheckedValue' => 'b']);

The case reported in #140 is not the only one. With 'checkedValue' => 'a' and 'uncheckedValue' => 'b':

  • 'value' => true set before checkedValue stores the default 'y' instead of 'a'. It renders as checked either way, so the mismatch only surfaces in getValue() and in hasValue(), which then reports false for a visibly checked box.
  • 'value' => false set before uncheckedValue stores 'n' instead of 'b', the same silent mismatch.
  • 'value' => 'b' is order-independent by accident: 'b' matches neither the default 'y' nor 'a', so the stale comparison happens to yield the correct unchecked state.
  • 'checked' => true never consulted the checked value at all, which left the element checked while getValue() returned null.

The boolean cases corrupt getValue() and hasValue() even where the checkbox happens to render correctly, so guarding setCheckedValue() alone would not have been enough.

Changes

The value is now the only state a checkbox keeps, and everything else is derived from it on read:

  • getValue() resolves a boolean value to the checked or unchecked value
  • isChecked() compares getValue() with getCheckedValue()
  • setChecked() sets the value instead of a separate flag
  • hasValue() delegates to isChecked()
  • the checked property is gone

After the change, all five scenarios above yield a single outcome across every attribute ordering.

Changes in behavior

setChecked() and the checked attribute now set the element's value. A checkbox that is checked without having been submitted therefore has a value: getValue() returns the checked value instead of null, and a checkbox that is both required and checked passes validation instead of failing with "This field is required."

fix #140

@jrauh01 jrauh01 self-assigned this Aug 3, 2026
@cla-bot cla-bot Bot added the cla/signed label Aug 3, 2026
@jrauh01
jrauh01 requested a review from lippserd August 6, 2026 14:22
Comment thread src/FormElement/CheckboxElement.php Resolved
Comment thread src/FormElement/CheckboxElement.php Resolved
Comment thread src/FormElement/CheckboxElement.php Resolved
@jrauh01
jrauh01 force-pushed the fix/checkbox-attribute-order branch from 721f18f to 0b4fb79 Compare August 7, 2026 10:57
@jrauh01
jrauh01 requested a review from lippserd August 7, 2026 10:58
@jrauh01
jrauh01 force-pushed the fix/checkbox-attribute-order branch from 96e7c30 to 96cee7e Compare August 7, 2026 18:06
@jrauh01
jrauh01 requested a review from lippserd August 7, 2026 18:07
@jrauh01
jrauh01 force-pushed the fix/checkbox-attribute-order branch from 96cee7e to 3db5fc2 Compare August 12, 2026 15:43
Setting `value` before `checkedValue` or `uncheckedValue` left the element
in a state derived from the defaults `y` and `n`: a checkbox whose value
matched its checked value rendered unchecked, and a boolean value got
resolved to `y` or `n` instead of the custom values. What an element
rendered and reported as its value therefore depended on the order of the
attributes passed to it, which callers have no reason to expect.

The value is now the only state a checkbox keeps. Booleans are resolved
when the value is read and the checked state is derived from it.

As a consequence `setChecked()` sets the element's value. A checkbox that
is checked without having been submitted hence has a value now, which lets
it pass validation if it is also required.
@jrauh01
jrauh01 force-pushed the fix/checkbox-attribute-order branch from 3db5fc2 to e87c2f1 Compare August 12, 2026 15:44

if (is_bool($value)) {
$value = $value ? $this->getCheckedValue() : $this->getUncheckedValue();
return $value ? $this->getCheckedValue() : $this->getUncheckedValue();

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please clear $this->valid in setCheckedValue() and setUncheckedValue(). These setters can now change the effective value without invalidating its cached validation result.

I reproduced this with value => true and a callback validator that accepts only 'y'. Initial validation passes. Calling setCheckedValue('a') changes the value to 'a'. isValid() still returns true. An explicit validate() call correctly rejects the new value. Before this change, the same sequence retained the validated value 'y'.

Add regression tests that validate the checkbox before changing a mapping. The next isValid() call should validate the new value automatically. Test that a valid checkbox becomes invalid after the mapping changes. Also test that an invalid checkbox becomes valid.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

$this->valid is now set to null in both methods. I've added three tests about it.

Because `getValue()` now resolves boolean values lazily, changing a value
mapping changes the effective value without the cached `$this->valid` knowing
about it. A subsequent `isValid()` call returns the stale result rather than
re-validating the new effective value.

Clear `$this->valid` in both setters so the next `isValid()` triggers a fresh
`validate()` run.
@jrauh01
jrauh01 force-pushed the fix/checkbox-attribute-order branch from 379a91e to 0ef8da1 Compare September 15, 2026 07:47
@jrauh01
jrauh01 requested a review from lippserd September 15, 2026 07:48
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Checkbox rendering depends on attribute order

2 participants