As discussed in http://groups.drupal.org/node/18019#comment-61970 and followup comments.
I have the thing working - the form part, not the 'throw exception on node_save' part, obviously. Still needs some polish, but seems to work great, and turned out to be much simpler than I anticipated (mostly because part of the work was already done with http://vcs.fourkitchens.com/drupal/7-fic/revision/433 - "abstract location of fields in the form structure so that form_alters won't break ahah behaviors (fieldgroup...)")
Comment | File | Size | Author |
---|---|---|---|
#39 | field-form-validation-369964-39.patch | 682 bytes | yched |
#33 | field-form-validation-369964-33.patch | 39.36 KB | yched |
#30 | field-form-validation-369964-30.patch | 39.41 KB | yched |
#23 | field-form-validation-369964-23.patch | 39.67 KB | yched |
#16 | field-form-validation-369964-16.patch | 33.61 KB | bjaspan |
Comments
Comment #1
yched CreditAttribution: yched commentedSo, a little background information :
CCK concepts reminder:
There are two levels of validation for fields in forms: widget-level validation, and field-level validation.
- Widget validation steps are specific to a given widget's own form structure and input format. They are executed through FAPI's #element_validate property.
Example : date textfield widget - is the input a valid PHP date() string ?
- Field validation steps are common to a given field type, independently of the specific widget being used in a given form, so that every widget doesn't have to reiplement them.
Example : number field - min and max allowed values.
Field validation happens in the field type's implementation of hook_field_validate(). By the time it is performed, widgets have already done their own validation, and, if needed, massaged the form input, so hook_field_validate() operates on data in the canonical 'field value' structure (the one you get form a loaded object). So, theoretically, the field-level validation *could* be perfomed on non-form, programmatic [node|user|...]_save() - more on this later.
Now, all of this is well and cleanly separated, except that validation errors detected at field-validation time have to be flagged back to an actual form element - and hook_field_validate() knows nothing about forms, or the actual widget that was used for the input. Because CCK D6 lived under the cumulated constraints of nodeapi and formAPI, it resorted to an ugly way of having widgets communicate to hook_field_validate() the location of the form element to flag (I wont't go into this, read about '_error_field' in http://groups.drupal.org/node/18019#comment-61984 if you're interested). Having calls to form_set_error() in hook_field_validate() also forbids validation on programmatic save().
Because we don't live under nodeapi anymore, the discussion in http://groups.drupal.org/node/18019 showed a better way of handling this. The attached patch implements the following model:
- hook_field_validate() is completely form agnostic. It receives field values, and returns an array of errors for those values. An error is: an error code (machine string) + a user-facing message.
- field_attach_validate() calls the various hook_field_validate() for all fields in the entity, and raises an exception if errors are found.
- actual form validation is done in a new field_attach_form_validate() function, that calls field_attach_validate(), catches the exception if any and transfers the errors back to the widgets for error flagging.
- The latter (transferring errors back to widgets) is done through field_default_form_errors(), which calls a new hook_field_widget_error(), implemented by widgets. They receive their form $element (fully built, with the '#parents' array that corresponds to their actual location in the form) and the $error, and simply call form_error() on the sub-element that is relevant according to their own internal logic. Using the error code, complex widgets can flag different errors on different sub-elements (e.g imagefield : 'file upload' element, or 'description' element).
From the POV of a 'fieldable entity' module (node, user, ...), this simply translates to:
1) in your _form_validate handler, call field_attach_form_validate() (insetad of previously field_attach_validate) to validate the fields in your forms.
2) *if* you want your programmatic saves to be validated too, then call field_attach_validate() in your [node|user|...]_save() function, and handle the exceptions whichever way makes sense for you. If you want the same function to be called for your forms submissions, then it probably needs to accept a boolean $validate flag as a param, so that validation is not done twice for forms.
The patch doesn't take any stab at implementing 2) for current core entities. Nothing in Drupal currently has non-form validation, and changing this is far outside the scope of the patch. But it makes the field API ready if we want to go there at some point, as well as for other non-core entities that might want to do so right now.
Note :
The patch converts node_form_validate() to the new model for field validation, but does not take care of user.module yet.
The patch also changes text and number field validation to reject the value '-1', so that it can be easily tested. Obviously those changes are not supposed to be committed.
Still marking as 'code needs review' - cos' that's what it needs right now.
Comment #2
yched CreditAttribution: yched commentedExact same patch, using unix linebreaks (I can't remember if I did the conversion for the patch above)
Comment #3
bjaspan CreditAttribution: bjaspan commentedI'm really psyched that you took this on. Thanks!
Notes on first-pass reading (I haven't actually tried applying the patch and running it yet):
Should field_attach_form_validate() actually modify its $object argument? I suspect that $object is a fake object anyway, created just for the purpose of calling this function. Do we even need to pass in a $object at all? We could just create a new stdClass object. Hmmmm... of course, we'd still need the id, revision id, and bundle it, and they'd be separate arguments. So maybe it is best to pass it a dummy object after all.
Instead of just failing with a bang (== WSOD), could you set an error on the form just not connected to a specific element?
It should be straightforward to add a test method to text.test etc. for field validation so we can remove the temporary comparison against -1, right?
Comment #4
yched CreditAttribution: yched commentedShould field_attach_form_validate() actually modify its $object argument?
Well, not actually really useful in this specific case because the $object will most likely be ditched right after the caller returns, but it's the general pattern for all other field_attach_*() functions, and since it does no harm, we're better off not creating an exception, IMO.
I suspect that $object is a fake object anyway, created just for the purpose of calling this function. Do we even need to pass in a $object at all? ... of course, we'd still need the id, revision id, and bundle it, and they'd be separate arguments. So maybe it is best to pass it a dummy object after all.
Just like for field_attach_submit(), it's built from submitted form values. There are good ways (field_test.module) and bad ways (node.module and its
$node = (object)$form_state['values']
) to build it, but how it's built is not relevant to us, we need an object with basic id keys. Again, taking an object is a matter of consistency with other field_attach_*() functions. I really do like they're being so consistent :-).Instead of just failing with a bang (== WSOD), could you set an error on the form just not connected to a specific element?
I went back and forth on this. Actually, a widget module without hook_field_widget_error() should be considered as "broken". I considered that minimal effort and 'not babysitting broken code' here means leaving the WSOD happen.
In fact, field.module could even provide a default fallback implementation of hook_field_widget_error() that would work in simple cases (like number and text fields), but I felt it would clutter field.form.inc more than anything else, plus it would be bad education since many contrib (or custom) field modules start out by deriving from those two, so it's important that they implement the hook.
Can definitely be discussed, but I'm fine with stating that the hook is "mandatory, period".
Patch rerolled.
Comment #5
bjaspan CreditAttribution: bjaspan commentedI accept all of yched's answers in #5. New patch attached:
* Made FieldValidationException a subclass of FieldException and moved the declarations around a bit.
* Improved the docs a bit.
I think we should add validation tests for the core field modules (I wanted to do this now but ran out of time), remove the hack -1 tests, and mark it rtbc. Or am I missing something?
Comment #6
yched CreditAttribution: yched commentedI think the plan is right. Actually, field.test already tests the error behavior on field_test fields, so the feature itself is tested. The '-1' hack on text and number fields was just for me (and reviewers) to be able to test it visually through actual forms and actual fields.
Text and number should have their tests at some point, not sure this is a blocker for this patch. Although, stating so would sure be an incentive to start those tests rather sooner than later :-). I'd personnally plan to write more form tests first (JS 'add more', for instance...)
Comment #7
yched CreditAttribution: yched commentedHm, I forgot that TODO item :
I *think* it's actually not needed anymore, but I'd need to check. Not critical, but it would probably be better to settle this before committing rather than in a followup patch.
Comment #8
yched CreditAttribution: yched commented"This class has no functionality of its own" - yet CRUD exceptions still directly invoke that class, right ?
Comment #9
bjaspan CreditAttribution: bjaspan commented#8: Yes, crud functions use that class. Not a problem. It is very common to derive and use an exception class for nothing but identifying the code that threw it. FieldValidationException is the exception (ha) in that it actually has a functional purpose.
Comment #10
bjaspan CreditAttribution: bjaspan commentedNew patch. I added testTextFieldValidation() to text.test and removed the -1 hack on text.module. I only test field_attach_validate(), not field_attach_form_validate(), because I haven't taken the time yet to really understand how the form processing works.
Comment #11
bjaspan CreditAttribution: bjaspan commentedRerolled.
Comment #12
yched CreditAttribution: yched commentedLol, I'm not the only one mixing my patches :-)
Rerolled, plus removed the bit mentioned in #7, which indeed is not needed anymore (remnant from when field_attach_submit handled form values extraction).
So, according to #5/#6, this should be RTBC ?
Comment #13
yched CreditAttribution: yched commentedright, with the patch this time.
Comment #14
bjaspan CreditAttribution: bjaspan commented+1
Comment #15
bjaspan CreditAttribution: bjaspan commentedThe latest patch does not include my changes from #5. Currently re-integrating.
Comment #16
bjaspan CreditAttribution: bjaspan commentedI re-integrated my changes from #5 and #10. No functional code changes. My changes are:
* Make FieldValidateException derive from FieldException, and move the class decls into different files/locations.
* Added/improved some phpdoc
* Add a text.module test case for max_length validation, only because I happened to have written it already.
Hopefully I'm attaching the right patch this time. :-)
Comment #17
yched CreditAttribution: yched commentedOh ? sorry, I was pretty sure I started off with your patch. Sorry about that.
Comment #18
bjaspan CreditAttribution: bjaspan commentedComment #19
webchickSorry, this patch really needs to be looked at by someone who did not help write it. :(
If you're around this weekend and very patient, and would be willing to walk me through it as a total dummy, I can try and review it. Otherwise, one of the other FiC sprint attendees is fine.
Comment #20
KarenS CreditAttribution: KarenS commentedSubscribing, I'll try to get back and do a review.
Comment #21
chx CreditAttribution: chx commentedFieldValidationException should be derived from FieldException, Barry fixed this already once or twice. Although I know we provide text module as a sort-of template for field authors still i would see some code saved by
and maybe add a comment to text that text_field_widget_error is not implemented because it uses the default. It looks pretty good otherwise.
Comment #22
yched CreditAttribution: yched commented"FieldValidationException extends FieldException" disappeared between #5 and #10 and never reappeared, along with a few doc changes.
I think I have caught all the bits and pieces now.
Currently reconsidering the if(!drupal_function_exists()) part. Not sure I'm convinced yet.
+ working on converting the user validation workflow. New patch to come.
Comment #23
yched CreditAttribution: yched commentedNew patch, gathers all the pieces that got lost in earlier patches, and updates field integration for user form submission workflow. On that aspect, user.module is even worse that node... Again, a user.module cleanup is outside the scope of field patches.
Comment #24
yched CreditAttribution: yched commentedAbout hook_field_widget_error() and drupal_function_exists() (comment #21) :
Widget modules should implement the hook. -1 on saving a few lines in text and number, by embedding some fallback code in field_default_form_errors() that hardcodes 'reasonable' assumptions on internal widget structure and field-type 'column' names ($element['value']). That would be the only place in the whole field.module. + adding a comment to text.module about why some hook is *not* implemented is awkard.
The patch in #23 does the following :
If the widget fails to implement hook_field_widget_error() (and thus is *broken*), we prevent the WSOD, and simply make sure the validation error is caught, even though not visually assigned to a specific form element, as Barry suggested in #3.
Comment #25
bjaspan CreditAttribution: bjaspan commentedI'm working on converting node body into a field. node.module supports "minimum number of words" for the body. Obviously we could build this into text.module, but in general if an entity type module adds specific fields to a bundle it might want to perform some kind of validation on the submitted data separate from whatever the fields perform.
The entity type can perform the validation entity_save() if it wants to and reject the save by throwing an exception. No problem.
However, if the entity wants to perform that validation during form submit, it needs a way to flag a failure on the correct form element. In node's case, it wants to flag an error on the body field's widget. Basically, it wants to do the equivalent of calling
with an errors array that says that the body field has an error. This isn't the right interface for an entity module to call, however. We should have a field_attach function for it.
This should probably be a follow-up patch but I just thought I'd mention it. :-)
Comment #26
bjaspan CreditAttribution: bjaspan commentedI did not mean to change the status.
Comment #27
yched CreditAttribution: yched commentedRight. This issue only refactors the field-type validation workflow, without any functional change. It doesn't change the fact that the validation checks are hardcoded in the field type (often parameterized by field or instance settings). "minimum number of words" could definitely be added to text fields.
The more general feature you mention is the (long standing) 'custom validation on fields' request. So +1 on brainstorming this in a separate issue ;-)
FWIW, we outlined a possible approach in #212665: Custom validating hook for CCK, initially as a GHOP proposal, more than a year ago, and didn't make any progress since then. In a CCK world, it was targeted at UI-defined validators (validator plugins), rather than 'run my custom validation PHP code', though. Going for the latter might simplify things a little.
PS : I started to work on rehauling / generalizing $node->build_mode and the $teaser boolean param. We'll probably meet at some point !
Comment #28
bjaspan CreditAttribution: bjaspan commentedThe last line in this snippet looks wrong; should it just be removed?
Comment #29
yched CreditAttribution: yched commentedCrap, yes, the commented out line should be removed, it's from a previous attempt. The previous line, that sets $element, can go too, it had no other purpose.
Comment #30
yched CreditAttribution: yched commentedRerolled, with the extraneous lines mentioned in #28-#29 removed.
Comment #32
yched CreditAttribution: yched commented1 fail in 'Image GD manipulation tests' - I don't think this is relevant to our patch... Plz retry, bot.
Comment #33
yched CreditAttribution: yched commentedNeeded a reroll after a minor comment change in HEAD.
I *really* think this is ready now...
Comment #34
KarenS CreditAttribution: KarenS commentedThis improvement in validation is badly needed. We were falling all over ourselves in CCK to handle validation correctly. I much prefer this method. I'm hoping to do an actual review later today or this weekend at the latest, but wanted to note my support for this solution.
Comment #35
KarenS CreditAttribution: KarenS commentedI installed this and tested it several ways. I tried the built-in tests. I installed the CCK UI and created a number field with a minimum and maximum allowed value and tested that it properly prevented me from adding invalid values in the node add form. I also created a node programmatically and tried to save it using node_save() with an invalid value and it correctly stripped out the value I tried to save. We don't have any method now to prevent a node_save if there are invalid values, so emptying the invalid value appears to be the logical result.
So I agree this is ready to commit.
Comment #36
yched CreditAttribution: yched commented"I also created a node programmatically and tried to save it using node_save() with an invalid value and it correctly stripped out the value I tried to save"
Are you sure about this ? From my own tests, 'invalid' values are *not* filtered out on programmatic save - and I really think we should leave it that way in this patch. From my comment #2 above :
"The patch doesn't take any stab at implementing [validation on programmatic save] for current core entities. Nothing in Drupal currently has non-form validation, and changing this is far outside the scope of the patch. But it makes the field API ready if we want to go there at some point, as well as for other non-core entities that might want to do so right now."
Comment #37
KarenS CreditAttribution: KarenS commentedYou are correct, sorry about that. The value didn't save because I set up my node_save() incorrectly. The validation function is never triggered at all during node_save(), which is the current behavior that we aren't trying to fix here.
This gave me more opportunities to try storing various values and track what happens to them, so I reiterate that I think the patch is fine and ready to commit. The patch *tester* was at fault :)
Comment #38
webchickThis patch is awesome. Anything that moves us further in the direction of *not* coupling our data APIs with our form APIs gets my +12032. The code is well-documented and clear. There were a few questions I found as I was reading through (thanks to yched for helping me step through), but they have more to do with the Field API than this patch specifically. I also cleaned up a few minor formatting/spacing errors prior to commit.
Committed to HEAD. Please reflect this in the Field API documentation.
Comment #39
yched CreditAttribution: yched commentedPatch missed one function call in _field_attach_validate() :
Attached patch removes the first call (deprecated). Pretty obvious, so I'm setting straight to RTBC.
Webchick: I don't think I see where documentation is still needed : the PHPdoc for hook_field_validate() is updated, and field_attach_validate() / field_attach_form_validate() have reasonably extensive docs. What other place are you thinking of ?
Comment #40
webchickUm. I guess I was thinking there was a Field API set of docs in the handbook, but it's actually all at api.drupal.org, so w00t!
Committed this follow-up to HEAD. :)
Comment #41
yched CreditAttribution: yched commentedRight, there's no such page currently, and there should definitely be one sooner rather than later...
Created #419348: Handbook for fields API