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...)")

Support from Acquia helps fund testing for Drupal Acquia logo

Comments

yched’s picture

So, 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.

yched’s picture

Status: Active » Needs review
FileSize
31.98 KB

Exact same patch, using unix linebreaks (I can't remember if I did the conversion for the patch above)

bjaspan’s picture

I'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):

+function _field_attach_form_validate($obj_type, &$object, $form, &$form_state) {
+  // Extract field values from submitted values.
+  _field_invoke_default('extract_form_values', $obj_type, $object, $form, $form_state);

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.

+    // Failing silently if the widget module does not implement
+    // hook_field_widget_error() would let the form pass validation. We choose
+    // to fail with a bang instead, and just take care of loading the function.
+    drupal_function_exists($function);

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?

yched’s picture

Should 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.

bjaspan’s picture

Status: Needs review » Needs work
FileSize
35.55 KB

I 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?

yched’s picture

I 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...)

yched’s picture

Hm, I forgot that TODO item :

function field_default_validate($obj_type, $object, $field, $instance, $items) {
(...)
+  // _field_invoke() does not add back items for fields not present in the
+  // original $object, so add them manually.
+  // TODO: needed ?
+  $object->{$field_name} = $items;
}

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.

yched’s picture

+ * Base class for all exceptions thrown by Field API functions.
+ *
+ * This class has no functionality of its own other than allowing all
+ * Field API exceptions to be caught by a single catch block.
+ */
+class FieldException extends Exception {}

"This class has no functionality of its own" - yet CRUD exceptions still directly invoke that class, right ?

bjaspan’s picture

#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.

bjaspan’s picture

New 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.

bjaspan’s picture

Status: Needs work » Needs review
FileSize
15.93 KB

Rerolled.

yched’s picture

Status: Needs review » Reviewed & tested by the community

Lol, 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 ?

yched’s picture

right, with the patch this time.

bjaspan’s picture

+1

bjaspan’s picture

Status: Reviewed & tested by the community » Needs work

The latest patch does not include my changes from #5. Currently re-integrating.

bjaspan’s picture

Status: Needs work » Needs review
FileSize
33.61 KB

I 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. :-)

yched’s picture

Oh ? sorry, I was pretty sure I started off with your patch. Sorry about that.

bjaspan’s picture

Status: Needs review » Reviewed & tested by the community
webchick’s picture

Status: Reviewed & tested by the community » Needs review

Sorry, 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.

KarenS’s picture

Subscribing, I'll try to get back and do a review.

chx’s picture

FieldValidationException 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

 $function = $instance['widget']['module'] . '_field_widget_error';
  if (!  drupal_function_exists($function) ) form_error($element['value'], $error['message']); 

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.

yched’s picture

Status: Needs review » Needs work

"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.

yched’s picture

Status: Needs work » Needs review
FileSize
39.67 KB

New 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.

yched’s picture

About 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.

bjaspan’s picture

Status: Needs review » Needs work

I'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

field_invoke_default('form_errors', $obj_type, $object, $form, $e->errors);

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. :-)

bjaspan’s picture

Status: Needs work » Needs review

I did not mean to change the status.

yched’s picture

Right. 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 !

bjaspan’s picture

The last line in this snippet looks wrong; should it just be removed?

-    // Filter out empty values.
-    $items = field_set_empty($field, $items);
+    $element = array('#parents' => array($field_name));
+    //form_set_value($element, $items, $form_state);
yched’s picture

Crap, 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.

yched’s picture

Rerolled, with the extraneous lines mentioned in #28-#29 removed.

Status: Needs review » Needs work

The last submitted patch failed testing.

yched’s picture

Status: Needs work » Needs review

1 fail in 'Image GD manipulation tests' - I don't think this is relevant to our patch... Plz retry, bot.

yched’s picture

Needed a reroll after a minor comment change in HEAD.
I *really* think this is ready now...

KarenS’s picture

This 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.

KarenS’s picture

Status: Needs review » Reviewed & tested by the community

I 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.

yched’s picture

"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."

KarenS’s picture

You 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 :)

webchick’s picture

Status: Reviewed & tested by the community » Needs work
Issue tags: +Needs documentation

This 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.

yched’s picture

Status: Needs work » Reviewed & tested by the community
FileSize
682 bytes

Patch missed one function call in _field_attach_validate() :

    $function = $module . '_field_attach_validate';
    $function($obj_type, $object, $form);
    $function($obj_type, $object, $errors);

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 ?

webchick’s picture

Status: Reviewed & tested by the community » Fixed

Um. 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. :)

yched’s picture

Right, there's no such page currently, and there should definitely be one sooner rather than later...
Created #419348: Handbook for fields API

Status: Fixed » Closed (fixed)
Issue tags: -Needs documentation

Automatically closed -- issue fixed for 2 weeks with no activity.