-
Notifications
You must be signed in to change notification settings - Fork 821
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
ENH Use FieldValidators for FormField validation #11449
base: 6
Are you sure you want to change the base?
ENH Use FieldValidators for FormField validation #11449
Conversation
cb45d46
to
e733757
Compare
e733757
to
aaba479
Compare
fc9d6db
to
bd8e82d
Compare
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Found while testing:
- ENH Use FieldValidators for FormField validation #11449 (comment)
- If I have a
TextField
with aDBInt
leaving it blank results in "Must be an integer" error even though it's not a required field. Setting numberic values works as expected. - Setting a value of
9999999999999999999999999999999999999999999999999999999999999
for aTextField
withDBInt
gives the error "Must be an integer" - Setting a value of
9999999999999999999999999999999999999999999999999999999999999
for aNumericField
withDBInt
gives the error "Must be numeric" - Setting
maxLength
on aNumericField
limits the length in the UI, but submitting a value that is too long does not result in a validation error. (doing the same withTextField
does result in a validation error) - Using
TextField
with aDBDate
if I set the value2024-01-24/124
I get a simple "error" toast (no validation error). In the network tab I see a 500 error with 'Uncaught InvalidArgumentException: Invalid date: '. Use y-MM-dd to prevent this error."- If I input
01-24-2025
I also get that exception but with a different message: "Invalid date: '01-24-2025'. Use y-MM-dd to prevent this error"
- If I input
- I get similar errors as above using
TextField
withDBDatetime
and withDBTime
and entering invalid values. - With
MoneyField
if I enter too many characters the validation message appears at the top of the page, instead of under the relevant form field. - With
MoneyField
if I enter an invalid currency amount the validation message appears at the top of the page, instead of under the relevant form field. - With
MoneyField
if I callsetAllowedCurrencies()
and set only one value, and there was an old value in the currency field before hand, saving the page results in a "Currency abc is not in the list of allowed currencies" validation error (and there's no way to change the value from the form).
Some of the errors above are likely covered under #11453 - if that's the case please just list which those are and we can deal with them in that other card.
Disabled and readonly fields
Users can't edit values in disabled or readonly fields, but those still get validated. Should we disable validation for readonly and disabled fields?
687480e
to
b596697
Compare
Have updated SingleSelectField and MultiSelectField to do casting string to int in use getValueForValidation() instead of setSubmittedValue(). This means that the internal value of the field stays and a numeric string
Have update FormField::setSubmittedValue() to alter blank strings to null
Note: in the PRs current state they now give "Must be an integer" validation messages The validation message comes from IntFieldValidator, which TextField and NumericField do not have, though DBInt does. The issue here is that it's above the max size of a signed 64 bit integer which is 9223372036854775807. DBInt has logic in setValue() to cast numeric string to int, however in the case of huge numbers it cannot, so it retains the value a numeric string, hence why it fails validation. Instead of failing to casting if we throw an InvalidArgumentException we just end up with a server error toast which is bad, and, throwing ValidationException in DBInt::setValue() is just wrong. The alternative would be to "truncate" the value to the max PHP int size, but that just defeats the whole purpose of all this validation work. So I don't really know what we can really do here to improve UX. While the message isn't great, it is enforcing data integrity by preventing saving a value to the database which cannot be stored.
Have added created NumericNonStringFieldValidator and moved the 'is_string() && is_numeric() to it, and gotten DBInt / DBDouble to extend that. NumericField uses the NumericStringFieldValidator so it does allow string values
These are all out of scope as this is existing behaviour. The exceptions are coming from DBDate/DBTime::setValue()
Have updated FieldValidationTrait to remove the subfield portion of the name so that the validation message now shows on the composite field
MoneyField is weird to begin with in that the currency field can either be a DropDownField (two or more allowed currencies) a HiddenField (one allowed currency) or a TextField (zero allowed currencies). The issue you had is that when there's only one allowed currency, it becomes a HiddenField. I've updated setValue() to swap out the currency field to a TextField if the currency being set is not in the allowed value list. This isn't ideal and I've rather do this in the MoneyField constructor, but when the form is rendering the value is set via setValue(), rather than as a contructor argument.
Out of scope as these are existing issues, e.g.
|
b596697
to
042bdc6
Compare
0753fe4
to
04e357a
Compare
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Still not done: #11449 (comment)
Numeric field problem
DBInt
with its default scaffolded form (NumericField
) and a value of "99999999999999999999999999999999999999999999999999999999999999999999" now says "Must be a string" as well as "Must be numeric"
It must not be a string, and it is numeric so these error messages are clearly wrong.
Regarding the "Must be numeric" part of this which I mentioned last review, you said
The issue here is that it's above the max size of a signed 64 bit integer which is 9223372036854775807. DBInt has logic in setValue() to cast numeric string to int, however in the case of huge numbers it cannot, so it retains the value a numeric string, hence why it fails validation.
One option to provide a more accurate validation message would be to check:
- Is the original value a numeric string? (
is_numeric('99999999999999999999999999999999999999999999999999999999999999999999')
will returntrue
even though it's larger than the int that can be represented by PHP) - Does casting the value to int result in a value different to the original? (e.g.
(int)$val == $val
will returntrue
if the value can be represented as a PHP int, butfalse
if it cannot)
Still not resolved from previous review
With MoneyField if I enter too many characters the validation message appears at the top of the page, instead of under the relevant form field.
With MoneyField if I enter an invalid currency amount the validation message appears at the top of the page, instead of under the relevant form field.
src/Forms/NumericField.php
Outdated
return (string) (int) $value; | ||
} | ||
return (string) (float) $value; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This feels wrong. What is the specific intention here? Casting to int and then to string seems pointless? Is this try/catch a non-standard conditional that could be replaced with a is_numeric()
check?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
$value could be anything, e.g. float, int, numeric string. There was existing logic to chop of decimal places if $this->getScale() is 0, hence the int cast.
I've updated the docblock description to say that the point of cast() is to get a numeric string and repect getScale()
Have changed try/catch to is_numeric()
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This feels wrong. What is the specific intention here? Casting to int and then to string seems pointless?
This part still applies. I still don't understand what the purpose of this is. Perhaps it just needs an inline code comment explaining it? But you might also need to explain it to me in person so I really get it. It's definitely weird and it's not clear what the intention is.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I've retained the logic though refactored the class and added a bunch of inline comments explaining all the things
src/Forms/NumericField.php
Outdated
} elseif ($value === false) { | ||
return 0; | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
- When would the value be explicitly
false
? - Why do we return
0
here, but cast returnsfalse
?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
false $value happens when the localisation formatter fails to parse the in setSubmittedValue()
dataValue() is passed to the DBField, we do not want to be settings the DBField value to false. 0 is appropriate.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We shouldn't be passing the wrong value into DBInt
. If someone sets an invalid value to the form field, we shouldn't be replacing whatever value used to be stored in the database with 0
Let it return false from here, and the validation logic in DBInt
will pick it up.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
That's true. Won't necessarily be DBInt as it could be any DBField e.g. DBVarchar. However NumericField::validate() will prevent code from even reaching this point so it doesn't really matter. I've expanded on the inline comment to explain this. In hindsight the only reason I was returning zero here was to get an existing unit test to pass, I've now updated the unit test to expect false instead of 0
04e357a
to
220c8e4
Compare
It's following the standard pattern of Update: have changed the
I've added "manual" validation to NumericField to check if It'll now simply give a validation message of "Invalid number", which is the same message if you enter "fish".
As noted here, that was previously resolved, currently works as it should
Even though the PR has added the validation, that's an existing issue. This is currently happened because of the newly added In order for this to show in the correct location |
dc73e81
to
a2c8366
Compare
I don't understand how having the |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Sorry, didn't start a review before adding comments.
Unresolved conversations:
a2c8366
to
e49e249
Compare
Issue #11422