FormErrorHandler.php 5.7 KB
Newer Older
1 2 3 4
<?php

namespace Drupal\Core\Form;

5
use Drupal\Component\Utility\NestedArray;
6
use Drupal\Core\Messenger\MessengerTrait;
7 8 9 10 11 12 13
use Drupal\Core\Render\Element;

/**
 * Handles form errors.
 */
class FormErrorHandler implements FormErrorHandlerInterface {

14 15
  use MessengerTrait;

16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42
  /**
   * {@inheritdoc}
   */
  public function handleFormErrors(array &$form, FormStateInterface $form_state) {
    // After validation check if there are errors.
    if ($errors = $form_state->getErrors()) {
      // Display error messages for each element.
      $this->displayErrorMessages($form, $form_state);

      // Loop through and assign each element its errors.
      $this->setElementErrorsFromFormState($form, $form_state);
    }

    return $this;
  }

  /**
   * Loops through and displays all form errors.
   *
   * @param array $form
   *   An associative array containing the structure of the form.
   * @param \Drupal\Core\Form\FormStateInterface $form_state
   *   The current state of the form.
   */
  protected function displayErrorMessages(array $form, FormStateInterface $form_state) {
    $errors = $form_state->getErrors();

43
    // Loop through all form errors and set an error message.
44
    foreach ($errors as $error) {
45
      $this->messenger()->addMessage($error, 'error');
46 47 48 49
    }
  }

  /**
50
   * Stores errors and a list of child element errors directly on each element.
51
   *
52 53 54 55
   * Grouping elements like containers, details, fieldgroups and fieldsets may
   * need error info from their child elements to be able to accessibly show
   * form error messages to a user. For example, a details element should be
   * opened when child elements have errors.
56
   *
57 58 59
   * Grouping example:
   * Assume you have a 'street' element somewhere in a form, which is displayed
   * in a details element 'address'. It might be:
60
   *
61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99
   * @code
   * $form['street'] = [
   *   '#type' => 'textfield',
   *   '#title' => $this->t('Street'),
   *   '#group' => 'address',
   *   '#required' => TRUE,
   * ];
   * $form['address'] = [
   *   '#type' => 'details',
   *   '#title' => $this->t('Address'),
   * ];
   * @endcode
   *
   * When submitting an empty street field, the generated error is available to
   * the different render elements like so:
   * @code
   * // The street textfield element.
   * $element = [
   *   '#errors' => {Drupal\Core\StringTranslation\TranslatableMarkup},
   *   '#children_errors' => [],
   * ];
   * // The address detail element.
   * $element = [
   *   '#errors' => null,
   *   '#children_errors' => [
   *      'street' => {Drupal\Core\StringTranslation\TranslatableMarkup}
   *   ],
   * ];
   * @endcode
   *
   * The list of child element errors of an element is an associative array. A
   * child element error is keyed with the #array_parents value of the
   * respective element. The key is formed by imploding this value with '][' as
   * glue. For example, a value ['contact_info', 'name'] becomes
   * 'contact_info][name'.
   *
   * @param array $form
   *   An associative array containing a reference to the complete structure of
   *   the form.
100 101
   * @param \Drupal\Core\Form\FormStateInterface $form_state
   *   The current state of the form.
102 103 104 105
   * @param array $elements
   *   An associative array containing the part of the form structure that will
   *   be processed while traversing up the tree. For recursion only; leave
   *   empty when calling this method.
106
   */
107 108 109 110 111 112 113 114 115 116
  protected function setElementErrorsFromFormState(array &$form, FormStateInterface $form_state, array &$elements = []) {
    // At the start of traversing up the form tree set the to be processed
    // elements to the complete form structure by reference so that we can
    // modify the original form. When processing grouped elements a reference to
    // the complete form is needed.
    if (empty($elements)) {
      $elements = &$form;
    }

    // Recurse through all element children.
117
    foreach (Element::children($elements) as $key) {
118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133
      if (!empty($elements[$key])) {
        // Get the child by reference so that we can update the original form.
        $child = &$elements[$key];

        // Call self to traverse up the form tree. The current element's child
        // contains the next elements to be processed.
        $this->setElementErrorsFromFormState($form, $form_state, $child);

        $children_errors = [];

        // Inherit all recorded "children errors" of the direct child.
        if (!empty($child['#children_errors'])) {
          $children_errors = $child['#children_errors'];
        }

        // Additionally store the errors of the direct child itself, keyed by
134
        // its parent elements structure.
135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159
        if (!empty($child['#errors'])) {
          $child_parents = implode('][', $child['#array_parents']);
          $children_errors[$child_parents] = $child['#errors'];
        }

        if (!empty($elements['#children_errors'])) {
          $elements['#children_errors'] += $children_errors;
        }
        else {
          $elements['#children_errors'] = $children_errors;
        }

        // If this direct child belongs to a group populate the grouping element
        // with the children errors.
        if (!empty($child['#group'])) {
          $parents = explode('][', $child['#group']);
          $group_element = NestedArray::getValue($form, $parents);
          if (isset($group_element['#children_errors'])) {
            $group_element['#children_errors'] = $group_element['#children_errors'] + $children_errors;
          }
          else {
            $group_element['#children_errors'] = $children_errors;
          }
          NestedArray::setValue($form, $parents, $group_element);
        }
160 161 162 163 164 165 166 167
      }
    }

    // Store the errors for this element on the element directly.
    $elements['#errors'] = $form_state->getError($elements);
  }

}