Problem/Motivation

  • In #1204230: Missing hook_field_widget_form_alter(), we added hook_field_widget_form_alter() and hook_field_widget_WIDGET_TYPE_form_alter(). Over the course of D8's refactoring of the entity field API, this hook lost the ability to affect the whole widget for multi-value fields.This is a regression as this was possible with the original patch (which was also backported to D7 as a contributed project blocker).
     
  • Currently, the hook is only invoked within WidgetBase::formSingleElement() and passed the $element array that corresponds to the single element. This means that for a field of N deltas, the hook is invoked N times, once per delta -- and never at the parent level.
     
  • Unfortunately, this means that this has been the behavior since 8.0.0, and so we need to continue to support it. (Sites might break in strange ways if suddenly multivalue widgets had the hook applied when implementations assumed only the child elements would be affected by the hook.)

This issue is a blocker for #2936293: At least inform content authors where they can list and add media and therefore major.

Proposed resolution

I can think of a few ways to solve this problem:

  1. Invoke the hook at the parent level as well. I couldn't really come up with a way to do this that wouldn't be disruptive for existing implementations, as the hook would suddenly start running in a case where it had not before and the developer did not expect. (It's not like our hooks definitions are an abstract an base class extended by modules' implementations.) :P
     
  2. Add two new hooks parallel to the existing hooks that only address the parent level:
    • hook_field_widget_multiple_form_alter()
    • hook_field_widget_multiple_WIDGET_TYPE_form_alter()
  3. Add two new hooks with a different name (not sure what) that affect both the parent and child levels, pass metadata about whether the particular invocation is the parent or child in $context, and deprecate the existing hooks.

We agreed to use approach #2.

Remaining tasks

User interface changes

None.

API changes

Data model changes

Shouldn't be any.

Comments

xjm created an issue. See original summary.

xjm’s picture

Status: Active » Needs review
StatusFileSize
new12.24 KB
xjm’s picture

Issue summary: View changes
xjm’s picture

Issue summary: View changes
xjm’s picture

Ah, as mentioned.

amateescu’s picture

I can't think of a way to make proposed resolution #1 work without breaking any existing code, so here's a review for the current patch:

  1. +++ b/core/lib/Drupal/Core/Field/WidgetBase.php
    @@ -241,8 +241,23 @@ protected function formMultipleElements(FieldItemListInterface $items, array &$f
    +      }
    +
    +    }
         return $elements;
    

    The empty line here needs to be moved after the last } :)

  2. +++ b/core/modules/field/field.api.php
    @@ -220,6 +222,77 @@ function hook_field_widget_WIDGET_TYPE_form_alter(&$element, \Drupal\Core\Form\F
    + *   - delta: The order of this item in the array of subelements (0, 1, 2, etc).
    

    We can not pass a value for the delta because multiple value widgets handle "all deltas" with a single form element.

  3. +++ b/core/modules/field/field.api.php
    @@ -220,6 +222,77 @@ function hook_field_widget_WIDGET_TYPE_form_alter(&$element, \Drupal\Core\Form\F
    + * @see \Drupal\Core\Field\WidgetBase::formSingleElement()
    ...
    + * @see \Drupal\Core\Field\WidgetBase::formSingleElement()
    

    This references needs to point to formMultipleElements() instead.

  4. +++ b/core/modules/media/media.module
    @@ -172,3 +173,125 @@ function media_form_field_ui_field_storage_add_form_alter(&$form, FormStateInter
    +  if (!empty($element['target_id']['#type']) && ($element['target_id']['#type'] === 'entity_autocomplete') && ($element['target_id']['#target_type'] === 'media')) {
    

    Since this hook is altering a specific widget (entity_reference_autocomplete), we don't need to check the #type of the form element.

  5. +++ b/core/modules/media/media.module
    @@ -172,3 +173,125 @@ function media_form_field_ui_field_storage_add_form_alter(&$form, FormStateInter
    +    // @todo There must be an easier way to get this.
    +    $cardinality = $context['items']->getFieldDefinition()->getFieldStorageDefinition()->getCardinality();
    

    How about setting a #cardinality property in \Drupal\Core\Field\WidgetBase::formSingleElement(), right at the top where we append various stuff to $element?

  6. +++ b/core/modules/media/media.module
    @@ -172,3 +173,125 @@ function media_form_field_ui_field_storage_add_form_alter(&$form, FormStateInter
    +function media_field_widget_multiple_entity_reference_autocomplete_form_alter(array &$elements, FormStateInterface $form_state, array $context) {
    

    The entity_reference_autocomplete widget does not handle multiple values, only entity_reference_autocomplete_tags does, so I assume this hook implementation is broken?

And as a general observation, the two hook implementations are duplicating quite a bit of non-trivial code (like access checking), is there a way to re-use some of that code?

xjm’s picture

Status: Needs review » Needs work

Thanks @amateescu!

xjm’s picture

Whoops the media stuff was not supposed to be in the patch at all. xjm git failure. I'll move the relevant bits of review etc. to the other issue. Thanks!

xjm’s picture

Status: Needs work » Needs review
Issue tags: -Needs subsystem maintainer review +Needs change record

We can not pass a value for the delta because multiple value widgets handle "all deltas" with a single form element.

Whoops, right, it was just whatever the last $delta was from the foreach over the individual values. Which is not right at all.

Attached addresses #7 (minus the things about the media implementation which weren't meant to be included here). I've made the cardinality available both as context for the alter hook and as part of the render element as suggested in #7.

Sounds like @amateescu is okay with the approach here, so untagging subsystem maintainer review (though of course further review is welcome). Thanks!

Still needs tests and a CR.

jonathanshaw’s picture

You forgot your attachment @xjm :)

xjm’s picture

StatusFileSize
new6.96 KB
new4.08 KB

🙄 derp

tedbow’s picture

+++ b/core/lib/Drupal/Core/Field/WidgetBase.php
@@ -241,8 +241,23 @@ protected function formMultipleElements(FieldItemListInterface $items, array &$f
+      if ($is_multiple) {
+        // Allow modules to alter the field multiple widget form element.
+        $context = [
+          'form' => $form,
+          'widget' => $this,
+          'items' => $items,
+          'delta' => $delta,
+          'default' => $this->isDefaultValueWidget($form_state),
+        ];
+        \Drupal::moduleHandler()->alter([
+          'field_widget_multiple_form',
+          'field_widget_multiple_' . $this->getPluginId() . '_form',
+        ], $elements, $form_state, $context);
+      }
+
+    }

It might need different name but what would be the downside calling the hook from this point always not just if $is_multiple == TRUE.

I thinking if I had some alter logic I wanted to apply to say all entity reference widgets and that logic covered both single value and multi-value fields I would have to implement this hook and hook_field_widget_WIDGET_TYPE_form_alter.

Where as if this was always fired then I could just implement this new hook.

Status: Needs review » Needs work

The last submitted patch, 12: field-2940201-10.patch, failed testing. View results

xjm’s picture

Status: Needs work » Needs review
StatusFileSize
new6.96 KB
new848 bytes
  1. +++ b/core/modules/field/field.api.php
    @@ -220,6 +224,78 @@ function hook_field_widget_WIDGET_TYPE_form_alter(&$element, \Drupal\Core\Form\F
    + * To adjust the individual widgets for the values themselves, use
    + * hook_field_widget_form_alter() instead.
    ...
    + * To modify the widget for individual items, use
    + * hook_field_widget_WIDGET_TYPE_form_alter() instead.
    

    If @tedbow's suggestion works out we'd want to update this paragraph appropriately, and possibly even deprecate the existing hooks' behavior as well.

  2. +++ b/core/modules/field/field.api.php
    @@ -220,6 +224,78 @@ function hook_field_widget_WIDGET_TYPE_form_alter(&$element, \Drupal\Core\Form\F
    + * @param array $elements
    ...
    + * @param array $context
    ...
    + * @param array $elements
    ...
    + * @param array $context
    

    These should use more specific data types than array; I'll fix this on the next reroll while I look into @tedbow's suggestion.

Meanwhile, fixing those pesky syntax errors.

xjm’s picture

Issue tags: -Needs change record

Drafted a CR; we can update it with example code once we finalize the implementation here: https://www.drupal.org/node/2940780

xjm’s picture

Assigned: Unassigned » xjm

I tested @tedbow's suggestion out on #2936293: At least inform content authors where they can list and add media and it works quite well, so I'll update that here.

xjm’s picture

StatusFileSize
new8 KB
new4.15 KB

Attached implements the new approach. This should allow much cleaner implementations that avoid the sort of code duplication @amateescu pointed out from the other issue.

I think we might want to completely deprecate the existing hooks since these will meet both usecases (https://www.drupal.org/core/deprecation#how-hook). I haven't done that yet here though since that probably needs a +1 from @amateescu.

Next: tests.

xjm’s picture

Probably actually should also just take the previous example code now and put it in the new examples with the Element::children() bit.

xjm’s picture

StatusFileSize
new9.13 KB
new3.09 KB

There is an existing test implementation of hook_field_widget_form_alter(), but as far as I can tell a lot of it is dead code. There's two places that do seem to use the implementation: Drupal\field\Tests\FormTest::testFieldFormSingle() and, oddly, Drupal\field_ui\Tests\ManageFieldsTest::helpDescriptions(). It looks like the remaining assertions started out in two of the much larger field tests in #1356824: No way for hook_field_widget_form_alter() to know if it is on a form or in defaults (prior to the Great Test Refactoring of 2012).

Just verifying that the other seemingly dead code in the implementation is actually dead code with the attached.

I also addressed #19.

benjifisher’s picture

It looks as though #1356824: No way for hook_field_widget_form_alter() to know if it is on a form or in defaults (currently NR for the D7 patch) added code to test hook_field_widget_form_alter() in field/lib/Drupal/field/Tests/FormTest.php and field_ui/lib/Drupal/field_ui/Tests/AlterTest.php, and then #1875992: Add EntityFormDisplay objects for entity forms removed AlterTest.php, and then #2084987: Remove usage of field_ui_default_value and recommend proper replacement added the code to ManageFieldsTest.php.

As far as I can tell from checking the git log, the second bit of test code was missing in between those two commits.

benjifisher’s picture

StatusFileSize
new13.38 KB
new4.55 KB

I hope this helps move the issue along a little.

I have implemented the two new hooks in the test module, adding some text to the descriptions. Then I test for the added text. I am having trouble running the tests locally, so I am not sure I got it right. :-(

Please see the comments in the test function.

Status: Needs review » Needs work

The last submitted patch, 22: field-2940201-22.patch, failed testing. View results

xjm’s picture

Thanks @benjifisher.

I'm debugging the patch a bit now.

xjm’s picture

Assigned: xjm » Unassigned
Status: Needs work » Needs review
StatusFileSize
new11.76 KB
new5.58 KB

Attached fixes the test and simplifies it a bit. I think it makes sense for this to be a separate test method. I've made it so that different elements are altered at the parent and child levels. I've also used an old trick with state variables to keep the hook from running during other tests.

There's potentially a lot more we could test here but I wanted to keep it simple given that this is a major blocker at the moment for an otherwise simple issue.

xjm’s picture

StatusFileSize
new13.59 KB
new4.61 KB

Attached sets it up to test both new hooks. I'm not sure the widget we're using is the right choice for testing multiple elements, since it appears that it concatenates a comma-separated list in a single field rather than having separate fields. But this will prove the basics are working either way.

Status: Needs review » Needs work

The last submitted patch, 26: field-interdiff-26.patch, failed testing. View results

xjm’s picture

Status: Needs work » Needs review

Oops. Gave the interdiff a patch extension. At least I can cancel the test again now!

amateescu’s picture

Edit: this review is for #25.

  1. +++ b/core/lib/Drupal/Core/Field/WidgetBase.php
    @@ -241,6 +241,19 @@ protected function formMultipleElements(FieldItemListInterface $items, array &$f
    +        'cardinality' => $cardinality,
    
    @@ -329,6 +344,7 @@ protected function formSingleElement(FieldItemListInterface $items, $delta, arra
    +        'cardinality' => $cardinality,
    
    +++ b/core/modules/field/field.api.php
    @@ -179,10 +184,13 @@ function hook_field_widget_info_alter(array &$info) {
    + *   - cardinality: The cardinality of the field in the field storage. See
    + *     \Drupal\Core\Field\FieldStorageDefinitionInterface for possible values.
    
    @@ -220,6 +234,76 @@ function hook_field_widget_WIDGET_TYPE_form_alter(&$element, \Drupal\Core\Form\F
    + *   - cardinality: The cardinality of the field in the field storage. See
    + *     \Drupal\Core\Field\FieldStorageDefinitionInterface for possible values.
    

    Not sure why we would include the cardinality as a context, isn't it enough that it is available in the $elements FAPI array?

  2. +++ b/core/modules/field/field.api.php
    @@ -220,6 +234,76 @@ function hook_field_widget_WIDGET_TYPE_form_alter(&$element, \Drupal\Core\Form\F
    + * \Drupal\Core\Render\Element::children().
    ...
    + * \Drupal\Core\Render\Element::children().
    

    Shouldn't we put $elements as the argument for children()?

  3. +++ b/core/modules/field/field.api.php
    @@ -220,6 +234,76 @@ function hook_field_widget_WIDGET_TYPE_form_alter(&$element, \Drupal\Core\Form\F
    + *   \Drupal\Core\Field\WidgetBaseInterface::formMultipleElements().
    ...
    + *   \Drupal\Core\Field\WidgetBaseInterface::formMultipleElements().
    

    WidgetBaseInterface does not have a formMultipleElements(), that's a protected method from WidgetBase.

  4. +++ b/core/modules/field/field.api.php
    @@ -220,6 +234,76 @@ function hook_field_widget_WIDGET_TYPE_form_alter(&$element, \Drupal\Core\Form\F
    + *   An associative array. See hook_field_widget_form_alter() for the structure
    

    I think this needs to reference hook_field_widget_multiple_form_alter() instead.

  5. +++ b/core/modules/field/field.api.php
    @@ -220,6 +234,76 @@ function hook_field_widget_WIDGET_TYPE_form_alter(&$element, \Drupal\Core\Form\F
    +  // hook_field_widget_mymodule_autocomplete_form_alter() will only act on
    

    Missing a 'multiple' in the example hook name.

  6. +++ b/core/modules/field/src/Tests/FormTest.php
    @@ -634,4 +634,29 @@ public function testLabelOnMultiValueFields() {
    +    \Drupal::state()->set('field_test.widget_form_alter', TRUE);
    
    +++ b/core/modules/field/tests/modules/field_test/field_test.module
    @@ -117,6 +108,21 @@ function field_test_field_widget_form_alter(&$element, FormStateInterface $form_
    +  if (\Drupal::state()->get('field_test.widget_form_alter')) {
    

    The name of this state entry makes it sounds like it's testing the existing widget_form_alter hook, not the new "multiple" one.

xjm’s picture

+++ b/core/modules/field/tests/modules/field_test/field_test.module
@@ -117,6 +108,45 @@ function field_test_field_widget_form_alter(&$element, FormStateInterface $form_
+  drupal_set_message($context['widget']->getPluginId());

This is cruft that can be removed; I was just making sure I named the other hook correctly. :P

xjm’s picture

StatusFileSize
new13.54 KB
new3.29 KB

Thanks @amateescu

Attached addresses #29.2-5 and #30.

Regarding point 1:

Not sure why we would include the cardinality as a context, isn't it enough that it is available in the $elements FAPI array?

I actually wasn't sure why we were including it in the FAPI array. The cardinality seems a cleaner place to add it. :) Left as-is for now pending further discussion.

Point 6 was already addressed by the previous patch, I think.

tedbow’s picture

Status: Needs review » Needs work

#29.1

Not sure why we would include the cardinality as a context, isn't it enough that it is available in the $elements FAPI array?

I agree with @amateescu that having it in the FAPI array is enough.

re @xjm

I actually wasn't sure why we were including it in the FAPI array. The cardinality seems a cleaner place to add it. :)

Did you mean "The context seems a cleaner place to add it."?

I think though it is already in the FAPI array so we should remove it. So I think removing it from the $context is better.

#29 2-5 were fixed

#29.6

The name of this state entry makes it sounds like it's testing the existing widget_form_alter hook, not the new "multiple" one.

Yes this is now fixed in the patch because the actual hook names are used as the state key.

So other than #29.1 this looks good to me.

larowlan’s picture

Another way around this is to use the one-per delta existing hook and add a process callback when the delta is 0. The process callback has access to the complete form by reference

ER in d7 used to do this for Ajax stuff

Would work without any code changes to core

berdir’s picture

I also think the "can no longer affect" part isn't really true, I don't think this is a regression. In 7.x, influencing the whole widget and not just a single item was much harder as it was not OOP, also for the widgets themself. The only thing that existed was the field behavior constants as used by https://api.drupal.org/api/drupal/modules%21image%21image.field.inc/func... for example.

https://api.drupal.org/api/drupal/modules%21field%21field.api.php/functi... also had delta context, and is called in the loop in https://api.drupal.org/api/drupal/modules%21field%21field.form.inc/funct....

What additionally existed was hook_field_attach_form(), but that is called on the whole form and not just a single field. And in 8.x, you can reproduce that by implement hook_form_alter() and then checking that $form_state->getFormObject() is a ContentEntityForm instance (and not a confirm form, to exclude delete and similar confirmation forms).

That said, I'm +1 on adding this hook, but it might "only" be a DX improvement, not a major bug/regression...

xjm’s picture

Assigned: Unassigned » xjm

Thanks @larowlan, @tedbow, and @Berdir. I know it worked in D7 somehow because I had to use it in a module. :) The delta was still there (and wrong) but the hook did get invoked at both the top and lower level.

Another way around this is to use the one-per delta existing hook and add a process callback when the delta is 0. The process callback has access to the complete form by reference.

Well this still doesn't allow modifying the outside wrapper. So I think the hook is till the way to go.

I think though it is already in the FAPI array so we should remove it. So I think removing it from the $context is better

Yeah I think this was probably a leftover from trying to work around the reduced scope of the hook, so I'll update that if this is what others think is best.

larowlan’s picture

Well this still doesn't allow modifying the outside wrapper. So I think the hook is till the way to go.

It does, a process callback gets the following signature - you have the whole form by reference

function myprocess_callback($element, FormStateInterface $form_state, array &$complete_form) {
  $array_parents = $element['#array_parents'];
  // Remove self.
  array_pop($array_parents);
  $wrapper = NestedArray::getValue($complete_form, $array_parents);
  // Have your way with the $wrapper.
  // ...
  // Set the value back.
  NestedArray::set($complete_form, $array_parents, $wrapper);
  return $element;
}

From FormBuilder

$element = call_user_func_array($form_state->prepareCallback($callback), [&$element, &$form_state, &$complete_form]);
xjm’s picture

Assigned: xjm » Unassigned
Status: Needs work » Needs review
StatusFileSize
new13.25 KB
new944 bytes

Here's the patch. (Sorry, wifi fail earlier.) We did confirm that this approach (without cardinality in the context) still works.

I do think the new hook is probably the way to go still.

tim.plunkett’s picture

This is one of those things that I see and ask myself "how did we get by without this?"
Which I think we also asked when hook_field_widget_form_alter() was added (also by @xjm!!)

The changes to existing code look good, the added hook looks good, the docs/example look good, and the comparison to the old hook is good.
RTBC!

tim.plunkett’s picture

Status: Needs review » Reviewed & tested by the community
Issue tags: -Needs tests
larowlan’s picture

Status: Reviewed & tested by the community » Needs review
Issue tags: +Needs issue summary update

Patch looks good, bugger that we have to add another hook, but the only other way around it at the moment is to use some Form API voodoo with process callbacks.

Can we get an IS update here, as the 'remaining tasks' seems out of date.

Thanks.

xjm’s picture

Issue summary: View changes
Status: Needs review » Reviewed & tested by the community

I've updated the IS and also the CR. Since these are small things, setting back to RTBC. :)

amateescu’s picture

StatusFileSize
new11.8 KB
new4.05 KB

Fixed a couple of things which were not addressed in #37.

xjm’s picture

Issue summary: View changes

Adding #2942097: Deprecate hook_field_widget_form_alter() in favor of hook_field_widget_multiple_form_alter() to the IS which I'd filed previously.

Interdiff in #42 looks good; thanks @amateescu.

xjm’s picture

Saving credit for the subsystem maintainer reviews and other reviewers.

xjm’s picture

Issue summary: View changes

Also fixing the "API changes" section to be more specific. :)

xjm’s picture

StatusFileSize
new11.93 KB
new9.04 KB

Per discussion with @amateescu, @catch, @berdir, and myself, I've renamed the hook to hook_field_widget_multivalue_form_alter() which I think is a little more clear. (@catch, @amateescu, and myself were in favor of said new name; @berdir was not totally in favor but also said he did not have a strong opinion.)

Probably need to let tests finish running in case I missed any bits to update there. I'll also update the CR.

Status: Reviewed & tested by the community » Needs work

The last submitted patch, 46: field-2940201-46.patch, failed testing. View results

xjm’s picture

Status: Needs work » Reviewed & tested by the community
StatusFileSize
new11.93 KB
new602 bytes

Hm yep had one search-and-replace too far. Fixed in attached. Setting straight back to RTBC since this change also had signoff from several folks.

Status: Reviewed & tested by the community » Needs work

The last submitted patch, 48: field-2940201-48.patch, failed testing. View results

xjm’s picture

Status: Needs work » Reviewed & tested by the community
StatusFileSize
new11.93 KB
new719 bytes
+++ b/core/lib/Drupal/Core/Field/WidgetBase.php
@@ -241,6 +241,19 @@ protected function formMultipleElements(FieldItemListInterface $items, array &$f
+        'field_widget_multiple_form',
+        'field_widget_multiple_' . $this->getPluginId() . '_form',

Duh.

But hey, I proved the test coverage works. Let's call that last a test-only patch. ;)

  • catch committed c924020 on 8.6.x
    Issue #2940201 by xjm, amateescu, benjifisher, tedbow, tim.plunkett,...
catch’s picture

Version: 8.6.x-dev » 8.5.x-dev
Status: Reviewed & tested by the community » Fixed

Committed/pushed to 8.6.x and cherry-picked to 8.5.x. Thanks!

  • catch committed 37f992e on 8.5.x
    Issue #2940201 by xjm, amateescu, benjifisher, tedbow, tim.plunkett,...

Status: Fixed » Closed (fixed)

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

ndf’s picture

The test added in this issue fails when applying #2915036: Display mode configurations don't get updated with new fields

The test added here fails when, while running the test-suite, entity_form_display are loaded via the entityTypeManager. Note that this is only loading of data, nothing is saved here!
In 2915036 this happens like this $this->entityTypeManager->getStorage('entity_form_display')->loadMultiple($form_mode_ids);.

Issue 2915036 loops over all form_displays after saving/updating to assure correct field settings.

I tried to understand what is happening in this test, but I have a really hard to time understanding it.

+++ b/core/modules/field/src/Tests/FormTest.php
@@ -634,4 +637,44 @@ public function testLabelOnMultiValueFields() {
+    FieldConfig::create($this->field)->save();
+    entity_get_form_display($this->field['entity_type'], $this->field['bundle'], 'default')
+      ->setComponent($field_name, [
+        'type' => 'test_field_widget_multiple',
+      ])
+      ->save();
+
+    $this->drupalGet('entity_test/add');
+    $this->assertUniqueText("From $hook(): prefix on field_test_text parent element.");
+    $this->assertText("From $hook(): description on field_test_text child element.");

Is this testing that a specific creating message in shown on the page?

If someone can give me advice where to take a next step that would be appreciated.