-
Notifications
You must be signed in to change notification settings - Fork 823
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
NEW: Add extension hook for field-specific validation #10569
NEW: Add extension hook for field-specific validation #10569
Conversation
{ | ||
$this->extend('updateValidationResult', $result, $validator); | ||
return $result; | ||
} |
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 is broken out into a separate method to save a bunch of duplication across FormField subclasses like:
$result = true;
$this->extend('updateValidationResult', $result, $validator);
return $result;
For some fields (like ConfirmedPasswordField
) this would end up being copy-pasted many times in one method, so it felt better to break it out like this.
The only other alternative I could spot would be to refactor all the validate()
methods to take this approach:
$result = true;
if (failedValidation()) {
$result = false;
}
$this->extend('updateValidationResult', $result, $validator);
return $result;
That works for simple fields (I essentially did that for TextField
for example) but for ConfirmedPasswordField
it means a big refactor and removing a bunch of early-returns, which just felt like a lot of work for no real gain in readability.
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 is a great solution - makes it really easy for third-party formfields to use this functionality.
This looks good... though I do have a small concern that it will be expected for this to "just work" for form fields, but then we'll implement a new field and forget to do this, or people will expect this to work for 3rd party fields which haven't done this, which leads to an inconsistent DX |
Even though it was touching a lot of files I was also sort of ok with the change, thinking about the same thing as Guy, i.e. how can we make sure this works most of the times across all custom fields and 3rd party modules and custom validators etc. |
Yeah I understand that, it’s kinda like using A) abusing the deprecation API; Open to suggestions 🙂 |
952ffba
to
7fef773
Compare
Have changed the target branch to 5.0 with a view to implementing option C above, but I don’t think there’s a way to do it that really makes sense. As field implementations don’t necessarily call I think it makes more sense to just leave this. If a developer doesn’t call |
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.
The reasoning makes sense to me, it's a ✅ from me but another pair of eyes would be good. There should be a corresponding docs PR linked, too, describing the addition and how to use it.
Docs PR: silverstripe/developer-docs#144 |
Bump |
I've approved this already but would prefer also @GuySartorelli's or @maxime-rainville's once over so we could get this to |
Sorry, I keep meaning to come back to this. On the balance I'm okay with this - but can you please include appropriate unit tests and update documentation (both the changelog and if we have validation docs there too) to tell people that this is a new best practice? I'll retarget this to |
7fef773
to
ee05f3f
Compare
Thanks @GuySartorelli, I’ve added a new test with a few assertions to cover this and have updated the docs pr to add a note to the 5.0.0 changelog, linking through to the validation docs which contain more detail |
Thanks @kinglozzer for taking the time and effort to get this polished and ready to be merged in, we do appreciate it! 👍 |
{ | ||
$this->extend('updateValidationResult', $result, $validator); | ||
return $result; | ||
} |
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 is a great solution - makes it really easy for third-party formfields to use this functionality.
7a451e8
to
b3e5ba9
Compare
Looks great, thank you! |
Weird, that doesn’t happen locally so perhaps another module being loaded for CI runs has a formfield I missed (proving that this test is necessary 😅). Annoyingly I can’t spot a way to add the current class being tested to any error messages that PHPUnit outputs, so I’ve hacked in some temporary debugging output |
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.
Slightly more robust and permanent debugging suggestion
Looks like the failure was from a test formfield.... may make sense to add a |
aab804e
to
b5785ba
Compare
// This block is not essential and only exists to make test debugging easier - without this, | ||
// the error message on failure is generic and doesn't include the class name that failed | ||
try { | ||
$invocationRule->verify(); |
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 API isn’t covered by phpunit’s BC promise, but it hasn’t changed from 8.x to 10.x: https://github.com/sebastianbergmann/phpunit/blob/10.0.9/src/Framework/MockObject/Rule/InvokedCount.php. I’ve included the comment for info so if it causes failures in future, this block can just be removed and the test will still run fine, just with less debugging info on error.
b5785ba
to
97f7be5
Compare
It should be okay now - the remaining failure for |
Nice. I've just rebased silverstripe/silverstripe-asset-admin#1305 and once that's green I'll merge it and then this rerun CI here (just in case there's some other PR further down the line that also needs to be updated). We also run framework tests against kitchen sink - have you had a look at whether the new test passes with all of kitchen sink installed? I'm guessing there are a few fields there that will need updating as well. |
No it doesn’t pass, and after looking at it I’m beginning to wonder though if it’s a good idea to iterate over all subclasses of Form fields for other modules might have their own argument requirements, even if they don’t need any changes following this PR they still have to be added to that An example is |
Yeah that does seem like a bit of an anti-pattern. But we will need a test like that somewhere in kitchen sink. We don't really have a repository that exists to hold unit tests like that.... I guess we could add that to frameworktest and configure kitchen sink to include unit tests from frameworktest... |
Use class_exists() within module for a class that exists in a non-framework module? We already do it quite a bit public function testMapInCMSGroupsNoLeftAndMain()
{
if (class_exists(LeftAndMain::class)) {
$this->markTestSkipped('LeftAndMain must not exist for this test.');
}
$result = Member::mapInCMSGroups();
$this->assertInstanceOf(Map::class, $result);
$this->assertEmpty($result, 'Without LeftAndMain, no groups are CMS groups.');
} We install silverstripe/installer for every github actions CI run, so the classes you're after will exist a lot of the time? Is that a valid way to solve this? |
The problem isn't that classes might not exist - the problem is that this test loops through all formfields, and when we have kitchen sink installed that means the framework unit test needs to include information about how to instantiate all of the kitchen sink formfields that have their own constructors. It may be that there's only a few and it doesn't make the test particularly messy. It may be that it makes the test quite a bit messier than it currently is. But mostly it means framework's unit test ends up having cases for a bunch of very-not-core classes. I think it's probably okay, given that it's a unit test so it won't be affecting anyone's project. But it's also not the ideal scenario. |
While it's messy i think it's much cleaner than introducing an entirely new paradigm of putting "shared" unit tests in separate module (frameworktest) |
I agree with that on the balance. And I also think this test is necessary. |
I’ve pushed a commit which covers everything in the kitchensink, I’ve raised a couple of new PRs for modules that did need this to be added (silverstripe/silverstripe-tagfield#234, silverstripe/silverstripe-subsites#510). |
Thank you! |
See also:
Rationale:
As far as I’m aware, these are currently the only ways you can perform custom validation on form fields:
validate()
Validator
and use that in place ofRequiredFields
These approaches all “work”, but they’re far from optimal. Adding a hook like this allows for cleaner project code, plus makes it easier to distribute validation functionality in modules.
Example:
This is a (slightly daft) example to illustrate the basic functionality this would offer: