Score:0

interaction between form element plugins and config entity plugin collections

by flag

I'm making a form element plugin which has compound elements, but which should return a single string value. This is the same idea as core's password confirm form element (https://api.drupal.org/api/drupal/core%21lib%21Drupal%21Core%21Render%21Element%21PasswordConfirm.php/10).

I've imitated what password confirm does: an #element_validate callback gets the value from the inner elements, and sets it as the value for the element as a whole:

    // Password field must be converted from a two-element array into a single
    // string regardless of validation results.
    $form_state->setValueForElement($element['pass1'], NULL);
    $form_state->setValueForElement($element['pass2'], NULL);
    $form_state->setValueForElement($element, $pass1);

This works fine in a plain form.

However, when I then try to use this form element in a config entity's form to select a plugin, it crashes on submit.

This is because when the form is rebuilt on submission, EntityForm::copyFormValuesToEntity() is called and calls $entity->getPluginCollections(), and at that point, the value of the main element is an array, which causes a crash in the plugin collection system because it tries to use that as an array key.

The value is an array because after main element is processed and the #element_validate callback sets sets its value to a string, the nested element is processed, and sets its value, and that causes NestedArray to create an array in the form values:

Step 1, form values are:

[
  main_element => value,
]

Step 2, inner element's value sets set on form state values and we get:

[
  main_element => [
    inner_element => value,
  ],
]

I don't understand why during rebuild we get this effect, but in the final submission, the value of the element is a string as expected.

Am I doing something wrong, or is this just not going to work?

Snippet of code for the form element:

  /**
   * Process callback.
   */
  public static function processPlugin(&$element, FormStateInterface $form_state, &$complete_form) {
    $element['#tree'] = TRUE;

    // This needs to be a nested element so the radio or select element
    // processing and theming takes place.
    $element['plugin_id'] = [
      '#type' => $element['#options_element_type'],
      '#title' => $element['#title'],
      '#options' => $options, // SNIP defined elsewhere
      '#empty_value' => '',
      '#required' => $element['#required'],
      '#default_value' => $element['#default_value'] ?? '',
    ];

    $element['#element_validate'] = [[static::class, 'validatePlugin']];

    return $element;
  }

  /**
   * {@inheritdoc}
   */
  public static function valueCallback(&$element, $input, FormStateInterface $form_state) {
    if ($input === FALSE) {
      return $element['#default_value'] ?? '';
    }
    elseif (is_null($input)) {
      return $element['#default_value'] ?? '';
    }
    else {
      return $input['plugin_id'];
    }
  }

  public static function validatePlugin(&$element, FormStateInterface $form_state, &$complete_form) {
    $plugin_id = $element['plugin_id']['#value'];

    // Set the value at the top level of the element.
    $form_state->setValueForElement($element['plugin_id'], NULL);
    $form_state->setValueForElement($element, $plugin_id);

    return $element;
  }

(Full thing is in this MR https://git.drupalcode.org/project/plugin/-/merge_requests/3#note_144104 for this issue https://www.drupal.org/project/plugin/issues/3197304.)

Snippet of code for the form that uses it:

    $form['link_style'] = [
      '#type' => 'action_link_plugin',
      '#title' => $this->t('Link style'),
      '#required' => TRUE,
      '#default_value' => $action_link->get('link_style'),
      '#plugin_type' => 'action_link.link_style',
      '#options_element_type' => 'radios',
    ];

And in the config entity class:


public function getPluginCollections() {
  $collections = [];
  if ($this->getActionLinkPluginCollection()) {
    $collections['plugin_config'] = $this->getActionLinkPluginCollection();
  }
  if ($this->getLinkStylePluginCollection()) {
    $collections['link_style_collection'] = $this->getLinkStylePluginCollection();
  }
  return $collections;
}

protected function getActionLinkPluginCollection() {
  if (!$this->actionLinkPluginCollection && $this->plugin_id) {
    $this->actionLinkPluginCollection = new DefaultSingleLazyPluginCollection(
      \Drupal::service('plugin.manager.action_link_state_action'),
      $this->plugin_id, $this->plugin_config
    );
  }
  return $this->actionLinkPluginCollection;
}

protected function getLinkStylePluginCollection() {
  // Horrible workaround for the form element's inner element's value getting
  // set and then the resulting value *array* for the outer element being used
  // by copyFormValuesToEntity().
  if (is_array($this->link_style)) {
    return NULL;
  }
  if (!$this->linkStylePluginCollection && $this->link_style) {
    $this->linkStylePluginCollection = new DefaultSingleLazyPluginCollection(
      \Drupal::service('plugin.manager.action_link_style'),
      $this->link_style,
      []
    );
  }
  return $this->linkStylePluginCollection;
}
cn flag
Please can you add a snippet of code to reproduce the problem to the question body? Links to code aren’t useful, as the question would not make sense in the future if/when that link goes dead
by flag
Will do, but it's a TON of code.
cn flag
We just need a representative sample here, enough to demo the problem, not the whole thing :) Please see https://stackoverflow.com/help/minimal-reproducible-example for an explanation
by flag
Done. As you can see, it's a lot of code that's involved.
I sit in a Tesla and translated this thread with Ai:

mangohost

Post an answer

Most people don’t grasp that asking a lot of questions unlocks learning and improves interpersonal bonding. In Alison’s studies, for example, though people could accurately recall how many questions had been asked in their conversations, they didn’t intuit the link between questions and liking. Across four studies, in which participants were engaged in conversations themselves or read transcripts of others’ conversations, people tended not to realize that question asking would influence—or had influenced—the level of amity between the conversationalists.