You are here

class MultiValue in SAML Authentication 4.x

Same name and namespace in other branches
  1. 8.3 src/Element/MultiValue.php \Drupal\samlauth\Element\MultiValue

Provides a multi-value form element.

Shamelessly copied from multivalue_form_element, on who we'll depend in 4.x. (At the time of adding this, they seemed to have issues with D9 compatibility.)

Properties:

  • #cardinality: the cardinality of this element. Can be a positive number or MultiValue::CARDINALITY_UNLIMITED to set it as unlimited. The default value is unlimited.
  • #add_empty: Applicable only for unlimited cardinality: 0 to add no extra empty sets values below existing values. FALSE to add no extra empty values, except display one when there are no existing values. By default, one empty value is added.
  • #add_more_label: the label to use for the "add more" button. The default value is "Add another item".

Use this element as a wrapper for other form elements. They will be repeated based on the cardinality specified, organised under a "delta", similar to field widgets. Deltas are sortable. Example of an element that allows to specify unlimited job title strings:

$form['job_titles'] = [
  '#type' => 'multivalue',
  '#title' => $this
    ->t('Job titles'),
  'title' => [
    '#type' => 'textfield',
    '#title' => $this
      ->t('Job title'),
    '#title_display' => 'invisible',
  ],
];

Example of an element with multiple form elements inside. Each "delta" will contain all the children of the main element. This example allows to specify up to three pairs of name/e-mail values:


$form['contacts'] = [
  '#type' => 'multivalue',
  '#title' => $this->t('Contacts'),
  '#cardinality' => 3,
  'name' => [
    '#type' => 'textfield',
    '#title' => $this->t('Name'),
  ],
  'mail' => [
    '#type' => 'email',
    '#title' => $this->t('E-mail'),
  ],
];
@endCode

Default values can be set to the multi-value form element. Never set them in
child elements as they will be overridden.
Pass the default values keyed by their delta:
@code
$form['contacts'] = [
  '#type' => 'multivalue',
  '#default_value' => [
    0 => ['name' => 'Bob', 'mail' => 'bob@example.com'],
    1 => ['name' => 'Ted', 'mail' => 'ted@example.com'],
  ],
  ...
];
@endCode

If only one child element is present, said child element name can be omitted
from the default value array:
@code
$form['job_titles'] = [
  '#type' => 'multivalue',
  '#title' => $this->t('Job titles'),
  'title' => [
    ...
  ],
  '#default_value' => [
    'Foo',
    'Bar',
  ],
];

Note that the values in the form state will always have the full array structure, including the child element name.

The element can be marked as required. The required will apply *only* to the first delta. This behaviour is consistent with entity fields. How child elements are marked as required depends on their own #required property. Given the multi-value element is marked as required:

  • if no children is marked as required, all the children of the first delta will be set as required.
  • if any children is marked as required, then the required status specified for the children will be retained for the first delta.

For all the deltas after the first, or when the main element is not marked as required, the #required property of the child elements will be set to FALSE.

Example of specifying only some elements are required:


$form['contacts'] = [
  '#type' => 'multivalue',
  '#title' => $this->t('Contacts'),
  '#required' => TRUE,
  'name' => [
    '#type' => 'textfield',
    '#title' => $this->t('Name'),
    '#required' => TRUE,
  ],
  'mail' => [
    '#type' => 'email',
    '#title' => $this->t('E-mail'),
  ],
];
@endCode

If you want to have some children required in all the deltas, use #states
to mark the wanted elements as required if one of the other children is
filled.

<h3>Plugin annotation</h3>
@code
@FormElement("samlmultivalue")

Hierarchy

Expanded class hierarchy of MultiValue

1 #type use of MultiValue
SamlauthConfigureForm::buildForm in src/Form/SamlauthConfigureForm.php
Form constructor.

File

src/Element/MultiValue.php, line 135

Namespace

Drupal\samlauth\Element
View source
class MultiValue extends FormElement {

  /**
   * Value indicating that an instance of this element accepts unlimited values.
   */
  const CARDINALITY_UNLIMITED = -1;

  /**
   * {@inheritdoc}
   */
  public function getInfo() {
    $class = get_class($this);
    return [
      '#input' => TRUE,
      '#theme' => 'field_multiple_value_form',
      '#cardinality_multiple' => TRUE,
      '#description' => NULL,
      '#cardinality' => self::CARDINALITY_UNLIMITED,
      '#add_more_label' => $this
        ->t('Add another item'),
      '#process' => [
        [
          $class,
          'processMultiValue',
        ],
        [
          $class,
          'processAjaxForm',
        ],
      ],
      '#element_validate' => [
        [
          $class,
          'validateMultiValue',
        ],
      ],
    ];
  }

  /**
   * Processes a multi-value form element.
   *
   * @param array $element
   *   The element being processed.
   * @param \Drupal\Core\Form\FormStateInterface $form_state
   *   The current form state.
   * @param array $complete_form
   *   The complete form.
   *
   * @return array
   *   The processed element.
   */
  public static function processMultiValue(array &$element, FormStateInterface $form_state, array &$complete_form) : array {
    $element_name = end($element['#array_parents']);
    $parents = $element['#parents'];
    $cardinality = $element['#cardinality'];
    $element['#tree'] = TRUE;
    $element['#field_name'] = $element_name;
    $element_state = static::getElementState($parents, $element_name, $form_state);
    if ($element_state === NULL) {

      // The default value should always have numeric keys. The initial count
      // is based on the default value... except if #add_empty says to add an
      // extra item only for 0 values.
      if (!$element['#default_value'] && isset($element['#add_empty']) && $element['#add_empty'] === FALSE) {
        $element_state = [
          'items_count' => 1,
        ];
      }
      else {
        $element_state = [
          'items_count' => count($element['#default_value'] ?? []),
        ];
      }
      static::setElementState($parents, $element_name, $form_state, $element_state);
    }

    // Determine the number of elements to display.
    if ($cardinality !== self::CARDINALITY_UNLIMITED) {
      $nr_elements = $cardinality;
    }
    elseif (!empty($element['#disabled']) || isset($element['#add_empty']) && !$element['#add_empty']) {
      $nr_elements = $element_state['items_count'];
    }
    else {
      $nr_elements = $element_state['items_count'] + 1;
    }

    // Extract the elements that will have to be repeated for each delta.
    $children = [];
    foreach (Element::children($element) as $child) {
      $children[$child] = $element[$child];
      unset($element[$child]);
    }
    $value = is_array($element['#value']) ? $element['#value'] : [];

    // Re-key the elements so that deltas are consecutive.
    $value = array_values($value);
    for ($i = 0; $i < $nr_elements; $i++) {
      $element[$i] = $children;
      if (isset($value[$i])) {
        static::setDefaultValue($element[$i], $value[$i]);
      }
      static::setRequiredProperty($element[$i], $i, $element['#required']);
      $element[$i]['_weight'] = [
        '#type' => 'weight',
        '#title' => t('Weight for row @number', [
          '@number' => $i + 1,
        ]),
        '#title_display' => 'invisible',
        '#default_value' => $i,
        '#weight' => 100,
      ];
    }
    if ($cardinality === self::CARDINALITY_UNLIMITED && !$form_state
      ->isProgrammed()) {
      $id_prefix = implode('-', $parents);
      $wrapper_id = Html::getUniqueId($id_prefix . '-add-more-wrapper');
      $element['#prefix'] = '<div id="' . $wrapper_id . '">';
      $element['#suffix'] = '</div>';
      $element['add_more'] = [
        '#type' => 'submit',
        '#name' => strtr($id_prefix, '-', '_') . '_add_more',
        '#value' => $element['#add_more_label'],
        '#attributes' => [
          'class' => [
            'multivalue-add-more-submit',
          ],
        ],
        '#limit_validation_errors' => [
          $element['#array_parents'],
        ],
        '#submit' => [
          [
            static::class,
            'addMoreSubmit',
          ],
        ],
        '#ajax' => [
          'callback' => [
            static::class,
            'addMoreAjax',
          ],
          'wrapper' => $wrapper_id,
          'effect' => 'fade',
        ],
      ];
    }
    return $element;
  }

  /**
   * Validates a multi-value form element.
   *
   * Used to clean and sort the submitted values in the form state.
   *
   * @param array $element
   *   The element being processed.
   * @param \Drupal\Core\Form\FormStateInterface $form_state
   *   The current form state.
   * @param array $complete_form
   *   The complete form.
   */
  public static function validateMultiValue(array &$element, FormStateInterface $form_state, array &$complete_form) : void {
    $input_exists = FALSE;
    $values = NestedArray::getValue($form_state
      ->getValues(), $element['#parents'], $input_exists);
    if (!$input_exists) {
      return;
    }

    // Remove the 'value' of the 'add more' button.
    unset($values['add_more']);

    // Sort the values based on the weight.
    usort($values, function ($a, $b) {
      return SortArray::sortByKeyInt($a, $b, '_weight');
    });
    foreach ($values as $delta => &$delta_values) {

      // Remove the weight element value from the submitted data.
      unset($delta_values['_weight']);

      // Determine if all the elements of this delta are empty.
      $is_empty_delta = array_reduce($delta_values, function (bool $carry, $value) : bool {
        if (is_array($value)) {
          return $carry && empty(array_filter($value));
        }
        else {
          return $carry && ($value === NULL || $value === '');
        }
      }, TRUE);

      // If all the elements are empty, drop this delta.
      if ($is_empty_delta) {
        unset($values[$delta]);
      }
    }

    // Re-key the elements so that deltas are consecutive.
    $values = array_values($values);

    // Set the value back to the form state.
    $form_state
      ->setValueForElement($element, $values);
  }

  /**
   * {@inheritdoc}
   */
  public static function valueCallback(&$element, $input, FormStateInterface $form_state) {
    if ($input !== FALSE) {
      return $input;
    }
    $value = [];
    $element += [
      '#default_value' => [],
    ];
    $children_keys = Element::children($element, FALSE);
    $first_child = reset($children_keys);
    $children_count = count($children_keys);
    foreach ($element['#default_value'] as $delta => $default_value) {

      // Enforce numeric deltas.
      if (!is_numeric($delta)) {
        continue;
      }

      // Allow to omit the child element name when one single child exists and
      // the values are simple literals. This allows to pass
      // [0 => 'value 1', 1 => 'value 2'] instead of
      // [0 => ['element_name' => 'value 1', 1 => ['element_name' => ...]].
      if ($children_count === 1 && !is_array($default_value)) {
        $value[$delta] = [
          $first_child => $default_value,
        ];
      }
      else {
        $value[$delta] = $default_value;
      }
    }
    return $value;
  }

  /**
   * Handles the "Add another item" button AJAX request.
   *
   * @param array $form
   *   The build form.
   * @param \Drupal\Core\Form\FormStateInterface $form_state
   *   The form state.
   *
   * @see \Drupal\Core\Field\WidgetBase::addMoreSubmit()
   */
  public static function addMoreSubmit(array $form, FormStateInterface $form_state) : void {
    $button = $form_state
      ->getTriggeringElement();

    // Go one level up in the form, to the widgets container.
    $element = NestedArray::getValue($form, array_slice($button['#array_parents'], 0, -1));
    $element_name = $element['#field_name'];
    $parents = $element['#parents'];

    // Increment the items count.
    $element_state = static::getElementState($parents, $element_name, $form_state);
    $element_state['items_count']++;
    static::setElementState($parents, $element_name, $form_state, $element_state);
    $form_state
      ->setRebuild();
  }

  /**
   * Ajax callback for the "Add another item" button.
   *
   * @param array $form
   *   The build form.
   * @param \Drupal\Core\Form\FormStateInterface $form_state
   *   The form state.
   *
   * @return array|null
   *   The element.
   *
   * @see \Drupal\Core\Field\WidgetBase::addMoreAjax()
   */
  public static function addMoreAjax(array $form, FormStateInterface $form_state) : ?array {
    $button = $form_state
      ->getTriggeringElement();

    // Go one level up in the form, to the widgets container.
    $element = NestedArray::getValue($form, array_slice($button['#array_parents'], 0, -1));

    // Ensure the widget allows adding additional items.
    if ($element['#cardinality'] != FieldStorageDefinitionInterface::CARDINALITY_UNLIMITED) {
      return NULL;
    }

    // Adding until #1091852 gets solved.
    if (!empty($element['#description_suffix'])) {
      $element['#description'] .= ' ' . $element['#description_suffix'];
    }
    return $element;
  }

  /**
   * Sets the default value for the child elements.
   *
   * @param array $elements
   *   The elements array.
   * @param array $value
   *   An array of values, keyed by the children element name.
   */
  public static function setDefaultValue(array &$elements, array $value) : void {

    // @todo Handle nested elements.
    foreach (Element::children($elements, FALSE) as $child) {
      if (isset($value[$child])) {
        $elements[$child]['#default_value'] = $value[$child];
      }
    }
  }

  /**
   * Sets the required property for the delta being processed.
   *
   * @param array $elements
   *   The array containing the child elements.
   * @param int $delta
   *   The delta currently being processed.
   * @param bool $required
   *   If the main element is required or not.
   */
  protected static function setRequiredProperty(array &$elements, int $delta, bool $required) : void {
    if ($delta === 0 && $required) {

      // If any of the children is set as required, the first delta is already
      // set correctly.
      foreach ($elements as $element) {
        if (isset($element['#required']) && $element['#required'] === TRUE) {
          return;
        }
      }

      // Set all children as required otherwise.
      foreach ($elements as &$element) {
        $element['#required'] = TRUE;
      }
      return;
    }

    // For every other delta or when the main element is marked as not required,
    // none of the children should be required neither.
    foreach ($elements as &$element) {
      $element['#required'] = FALSE;
    }
  }

  /**
   * Retrieves processing information about the element from $form_state.
   *
   * This method is static so that it can be used in static Form API callbacks.
   *
   * @param array $parents
   *   The array of #parents where the element lives in the form.
   * @param string $element_name
   *   The field name.
   * @param \Drupal\Core\Form\FormStateInterface $form_state
   *   The form state.
   *
   * @return array
   *   An array with the following key/value pairs:
   *   - items_count: The number of sub-elements to display for the element.
   *
   * @see \Drupal\Core\Field\WidgetBase::getWidgetState()
   */
  public static function getElementState(array $parents, string $element_name, FormStateInterface $form_state) : ?array {
    return NestedArray::getValue($form_state
      ->getStorage(), static::getElementStateParents($parents, $element_name));
  }

  /**
   * Stores processing information about the element in $form_state.
   *
   * This method is static so that it can be used in static Form API #callbacks.
   *
   * @param array $parents
   *   The array of #parents where the element lives in the form.
   * @param string $element_name
   *   The element name.
   * @param \Drupal\Core\Form\FormStateInterface $form_state
   *   The form state.
   * @param array $field_state
   *   The array of data to store. See getElementState() for the structure and
   *   content of the array.
   *
   * @see \Drupal\Core\Field\WidgetBase::setWidgetState()
   */
  public static function setElementState(array $parents, string $element_name, FormStateInterface $form_state, array $field_state) : void {
    NestedArray::setValue($form_state
      ->getStorage(), static::getElementStateParents($parents, $element_name), $field_state);
  }

  /**
   * Returns the location of processing information within $form_state.
   *
   * @param array $parents
   *   The array of #parents where the element lives in the form.
   * @param string $element_name
   *   The element name.
   *
   * @return array
   *   The location of processing information within $form_state.
   *
   * @see \Drupal\Core\Field\WidgetBase::getWidgetStateParents()
   */
  protected static function getElementStateParents(array $parents, string $element_name) : array {

    // phpcs:disable
    // Element processing data is placed at
    // $form_state->get(['multivalue_form_element_storage', '#parents', ...$parents..., '#elements', $element_name]),
    // to avoid clashes between field names and $parents parts.
    // phpcs:enable
    return array_merge([
      'multivalue_form_element_storage',
      '#parents',
    ], $parents, [
      '#elements',
      $element_name,
    ]);
  }

}

Members

Namesort descending Modifiers Type Description Overrides
DependencySerializationTrait::$_entityStorages protected property
DependencySerializationTrait::$_serviceIds protected property
DependencySerializationTrait::__sleep public function 2
DependencySerializationTrait::__wakeup public function 2
FormElement::processAutocomplete public static function Adds autocomplete functionality to elements.
FormElement::processPattern public static function #process callback for #pattern form element property.
FormElement::validatePattern public static function #element_validate callback for #pattern form element property.
MessengerTrait::$messenger protected property The messenger. 27
MessengerTrait::messenger public function Gets the messenger. 27
MessengerTrait::setMessenger public function Sets the messenger.
MultiValue::addMoreAjax public static function Ajax callback for the "Add another item" button.
MultiValue::addMoreSubmit public static function Handles the "Add another item" button AJAX request.
MultiValue::CARDINALITY_UNLIMITED constant Value indicating that an instance of this element accepts unlimited values.
MultiValue::getElementState public static function Retrieves processing information about the element from $form_state.
MultiValue::getElementStateParents protected static function Returns the location of processing information within $form_state.
MultiValue::getInfo public function Returns the element properties for this element. Overrides ElementInterface::getInfo
MultiValue::processMultiValue public static function Processes a multi-value form element.
MultiValue::setDefaultValue public static function Sets the default value for the child elements.
MultiValue::setElementState public static function Stores processing information about the element in $form_state.
MultiValue::setRequiredProperty protected static function Sets the required property for the delta being processed.
MultiValue::validateMultiValue public static function Validates a multi-value form element.
MultiValue::valueCallback public static function Determines how user input is mapped to an element's #value property. Overrides FormElement::valueCallback
PluginBase::$configuration protected property Configuration information passed into the plugin. 1
PluginBase::$pluginDefinition protected property The plugin implementation definition. 1
PluginBase::$pluginId protected property The plugin_id.
PluginBase::DERIVATIVE_SEPARATOR constant A string which is used to separate base plugin IDs from the derivative ID.
PluginBase::getBaseId public function Gets the base_plugin_id of the plugin instance. Overrides DerivativeInspectionInterface::getBaseId
PluginBase::getDerivativeId public function Gets the derivative_id of the plugin instance. Overrides DerivativeInspectionInterface::getDerivativeId
PluginBase::getPluginDefinition public function Gets the definition of the plugin implementation. Overrides PluginInspectionInterface::getPluginDefinition 2
PluginBase::getPluginId public function Gets the plugin_id of the plugin instance. Overrides PluginInspectionInterface::getPluginId
PluginBase::isConfigurable public function Determines if the plugin is configurable.
PluginBase::__construct public function Constructs a \Drupal\Component\Plugin\PluginBase object. 98
RenderElement::preRenderAjaxForm public static function Adds Ajax information about an element to communicate with JavaScript.
RenderElement::preRenderGroup public static function Adds members of this group as actual elements for rendering.
RenderElement::processAjaxForm public static function Form element processing handler for the #ajax form property. 1
RenderElement::processGroup public static function Arranges elements into groups.
RenderElement::setAttributes public static function Sets a form element's class attribute. Overrides ElementInterface::setAttributes
StringTranslationTrait::$stringTranslation protected property The string translation service. 4
StringTranslationTrait::formatPlural protected function Formats a string containing a count of items.
StringTranslationTrait::getNumberOfPlurals protected function Returns the number of plurals supported by a given language.
StringTranslationTrait::getStringTranslation protected function Gets the string translation service.
StringTranslationTrait::setStringTranslation public function Sets the string translation service to use. 2
StringTranslationTrait::t protected function Translates a string to the current language or to a given language.