Spin off from #942310: Field form cannot be attached more than once.

Field API form handling stores working data in $form_state['field']. For each field in the form, $form_state['field'][$field_name][$language] contains information about the widget, most notably the $field and $instance definitions - some widgets use helper callbacks (#value_callback, #element_validate...) that need to access those, and currently do so by reading $form_state['field'] directly.

This structure of $form_state['field'] has a couple limitations for advanced but important use cases including "combofield / multigroup". Those limitations are being worked out in #942310: Field form cannot be attached more than once, but there's no guarantee that this thread will reach RTBC status before next beta or RC.

Attached patch intends to do the API change now rather than later, by encapsulating access to Field API $form_state data into an API function. This gives us flexibility for later (possibly, but hopefully not, in a point release) changing the way $form_state['field'] is structured without breaking existing widget modules.

The API change affects widgets using helper 'element' FAPI callbacks that need to access $field or $instance properties.

Support from Acquia helps fund testing for Drupal Acquia logo

Comments

yched’s picture

Title: Abstract Fiald API $form_state use into an API func » Abstract Field API $form_state use into an API func

Title typo.

an.droid’s picture

Great!
Subscribing.

@@ -335,7 +335,7 @@ function image_field_widget_process($element, &$form_state, $form) {
   $item = $element['#value'];
   $item['fid'] = $element['fid']['#value'];
 
-  $instance = $form_state['field'][$element['#field_name']][$element['#language']]['instance'];
+  $instance = field_info_form_instance($element, $form_state);

Does field_info_form_instance() actually exists?
I didn't found it in this patch.

yched’s picture

FileSize
5.63 KB

Oops, that was a search/replace leftover after a 1st different version.
Fixed.

effulgentsia’s picture

Status: Active » Needs review

Subscribe. Having written the very hacky Flexifield module for D6, I'm hugely in favor of making a non-hacky Combofield module possible in D7.

But this clearly won't get in before beta2. Will need to review more thoroughly, and think about if there's a way to not break BC unnecessarily.

sun’s picture

Title: Abstract Field API $form_state use into an API func » Abstract Field API $form_state usage into an API function
an.droid’s picture

Will need to review more thoroughly, and think about if there's a way to not break BC unnecessarily.

We've discussed this in #942310: Field form cannot be attached more than once.
The problem is that current structure of $form_state['field'] doesn't allows to handle multiple instances of the same field in one form. And there is no way to fix it without API changes because this structure is hardcoded in widget callbacks. E.g.:

@@ -355,8 +355,9 @@ function number_field_widget_form(&$form, &$form_state, $field, $instance, $lang
  * FAPI validation of an individual number element.
  */
 function number_field_widget_validate($element, &$form_state) {
-  $field = $form_state['field'][$element['#field_name']][$element['#language']]['field'];
-  $instance = $form_state['field'][$element['#field_name']][$element['#language']]['instance'];
+  $info = field_form_info($element, $form_state);
+  $field = $info['field'];
+  $instance = $info['instance'];

Proposed solution doesn't changes structure of $form_state['field']. It just changes the way for widgets to retrieve needed information. And also makes it possible to change that structure later without breaking existing widgets.


@all,
if we want to truly abstract Field API from $form_state then we possibly need both 'getter' and 'setter' functions dealing with $form_state['field'] information. E.g.

function field_form_get_info($element, $form_state) { ... }
function field_form_set_info($element, &$form_state) { ... }
yched’s picture

I don't know about that. I can't really think of a use case, outside of Field API's internal form handling, where writing to $form_state['field'] would be needed. Designing an API func without a clear use cases in mind is a bit hazardous.

Currently $form_state['field'] is not something that we want to present to the outside world as "go ahead, write and modify stuff in there" by promoting an API func.

Unless there's a strong demand for a "write" counterpart, I'd rather stick to the read-only function for now.

fago’s picture

As yched explained, we need to have this data in the form as $field, $instance can be different from the stored field API for a widget shown. If it might be different, per-shown-widget, I'd say the data belongs to the form element.

However, as yched also has noted in the original issue, it was like that previously and it just bloated $form. So what about just using the field/instance names there to retrieve the stored definitions with a little helper + add in a possibility to override this "default", e.g. if #instance set too, use that. Then for the common case we don't have the form bloated.

Or, instead of looking for #instance, look for #instance_overrides and apply them in the helper function.

fago’s picture

Anyway, what is important is to make this change asap regardless which logic we want to follow later.

field_form_info() looks good to me, I wonder though whether we want to make use of list() instead? Or do we have the currently returned $info structure already anywhere?

yched’s picture

re @fago #8 : "if #instance is set use it, else read the official definition with field_info_instance()"

Sounds a bit convoluted IMO, and I'm not sure it's doable to begin with :
hook_field_default_form() receives a $field and an $instance, I don't see it diffing them against the field_info_*() versions to check whether it needs to set #field or #instance.

yched’s picture

re @fago #9 :

$info = field_form_info($element, $form_state);
$field = $info['field'];
$instance = $info['instance'];

vs.

list($field, $instance) = field_form_info($element, $form_state);

I do have a preference for the former, but no strong opinion. IIRC, list()-based API funcs are harder to sell to Dries and webchick.

effulgentsia’s picture

Ok. I finally took some more time with this patch. So it actually looks like it sort of maintains BC. Widgets should call the new field_form_info() function. But any contrib widget types that currently still access $form_state['field'][$element['#field_name']][$element['#language']] directly will continue to work. This addresses my concern from #4, so I'm okay with this being RTBC'd if the rest of you are good with it.

Regardless of whether this is committed now on a fast track (to give widget modules a chance to start using the "right" way prior to D7RC1), or done as part of the larger work in #942310: Field form cannot be attached more than once, I think it's very important that the larger issue maintain this BC. Assume there will be some contrib code out there that doesn't use the new function. It should still work with all core forms. Yeah, it might break when used in conjunction with a module that implements multiple field instances with the same name and different settings on the same form, but okay, that's when it can be pressured to be fixed.

fago’s picture

ad #11:
Dries and webchick in particular argued about returning a list of different things (return value + status) is bad, not using list() in general. As it's PHP's way of returning multiple things I think we should use that instead of inventing yet another array structure.

yched’s picture

Here's a version with list() construct.

moshe weitzman’s picture

Status: Needs review » Reviewed & tested by the community

Ready to fly then.

sun’s picture

Could we rename the helper function to field_get_form_info() or similar? field_form_info() sounds like a info hook name, and actually, http://drupal.org/project/form module is using hook_form_info() to turn forms into first-class citizens. Given module_implements_alter(), I could surely work around that, but would of course prefer to not have to.

yched’s picture

Status: Reviewed & tested by the community » Needs work

makes sense - will reroll tomorrow.

Dries’s picture

Status: Needs work » Needs review

Let's put this back to review so others can comment on sun's comment in #16. Like sun, I struggled with the name field_form_info() a bit as well.

It also makes me wonder why we have two functions in case A (i.e. field_info_field() / field_info_instance()) but only one function in case B (i.e. field_form_info()). It seems like this could potentially be made more consistent and more self-explanatory.

Having said that, I'd be OK with this patch. The only thing that comes to mind is to explain _why_ the $field and $instance arrays might differ from the official definitions returned by field_info_field() / field_info_instance(). Most people might not understand that.

It also made me wonder whether we need to update the documentation of field_info_field() / field_info_instance() to clarify when they should be used.

I think we have a bit more brainstorming to do. :)

yched’s picture

Turned into 2 functions as per Dries' request,
and changed the functions names so that:
- they are more inline with field_info_field() / field_info_instance(),
- they are connected to the field_widget_* namespace, since those are helper functions dedicated for widgets (the params intentionally only make sense in a form context)
- they don't clash with the (internal) "get field form info from $form_state" helper functions I'm preparing in #942310: Field form cannot be attached more than once

So, meet field_widget_field() / field_widget_instance() :

-  $field = $form_state['field'][$element['#field_name']][$element['#language']]['field'];
-  $instance = $form_state['field'][$element['#field_name']][$element['#language']]['instance'];
+  $field = field_widget_field($element, $form_state);
+  $instance = field_widget_instance($element, $form_state);

Also, expanded a bit the phpdoc for hook_field_widget_form(), which already tried to explain why field_info_*() are forbidden in widget processing functions.

an.droid’s picture

Just fixed a couple of typos in comment to hook_field_widget_form().

yched’s picture

Same as #20, without the "/no newline at end of file" bit.

moshe weitzman’s picture

Status: Needs review » Reviewed & tested by the community

This is a pretty straightforward code refactor. Nice docs as well.

Dries’s picture

Status: Reviewed & tested by the community » Fixed

This looks great now. Thanks yched. Committed to CVS HEAD.

yched’s picture

Thks Dries.

The API change should be announced on the dev list. I'll be posting a summary for rfay ASAP, need to run right now.

effulgentsia’s picture

This is great! Thank you, yched and an.droid, for the work on this issue and the other one. It'll be great to see combofield a reality in D7.

When announcing this to the dev list, as per #12, I really urge couching it as "it would be best for your widget modules to change from using $form_state['field'] to using the new functions". And perhaps explaining how this will ensure that the widget modules work correctly when combofield and other advanced forms become available during the D7 contrib cycle. But not as "you must do this". A lot of contrib developers have already been getting very frustrated at breaks between alpha and beta, and breaks between beta1 and beta2. We don't want people regretting that they have taken the #d7cx pledge and trying to make good on it. Just my suggestion. Feel free to take it or leave it.

yched’s picture

@effulgentia : it should be possible to preserve BC for the 'regular' case of fields at the $form top-level, at the cost of a small self-contained special case in an API func. More on that soon (promise !) in #942310: Field form cannot be attached more than once

yched’s picture

Status: Reviewed & tested by the community » Fixed

Here's the "API change" summary.
Possibly a bit verbose, anyone feel free to propose shorter versions if you feel it's needed ?
Also not sure whether the last bit ("plz wait for RC1/beta3 to apply the change") is needed or desirable.

----------------------
"New field_widget_field() / field_widget_instance() API functions to encapsulate access to Field API $form_state data"

The change affects modules defining Field "widgets" that rely on "$element callbacks" for their processing (#process, #element_validate, #value_callback...).

If these callbacks need to access properties of the $field or $instance definitions, they cannot use field_info_field() or field_info_instance() for the reasons explained in http://api.drupal.org/api/function/hook_field_widget_form/7. The recommended way so far to access $field and $instance definitions from those callbacks was to read them from $form_state['field'] :

$field = $form_state['field'][$element['#field_name']][$element['#language']]['field'];
// and / or :
$instance = $form_state['field'][$element['#field_name']][$element['#language']]['instance'];

The two new field_widget_field() / field_widget_instance() API functions encapsulate access to the data in $form_state['field']. The code above should to be changed into :

$field = field_widget_field($element, $form_state);
// and / or :
$instance = field_widget_instance($element, $form_state);

The actual structure of $form_state['field'] has not changed for now, but might change in the near future, in order to support more advanced combinations of fields within forms in contrib modules ("combofield" aka "fieldable field" aka D6 "multigroups", entity subform within an entity form...).
Widget modules that do not apply the change above will therefore not break when used in 'regular' entity forms, but will break in the more advanced cases mentioned above.

It is recommended that module maintainers wait for the next D7 release (beta3 or RC1) to apply the change, in order to avoid "undefined function" fatal errors for users testing the modules with D7 beta2.

yched’s picture

Status: Fixed » Reviewed & tested by the community
FileSize
667 bytes

Oops, small linefeed error in the phpdoc for hook_field_widget_form().
Trivial, RTBC. Sorry about that.

tom_o_t’s picture

Status: Fixed » Reviewed & tested by the community

#28: field_form_state_info-doc.patch queued for re-testing.

webchick’s picture

Status: Reviewed & tested by the community » Fixed

Committed to HEAD. Thanks!

Status: Fixed » Closed (fixed)
Issue tags: -API change

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