FormErrorHandler.php 5.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172
  1. <?php
  2. namespace Drupal\Core\Form;
  3. use Drupal\Component\Utility\NestedArray;
  4. use Drupal\Core\Render\Element;
  5. /**
  6. * Handles form errors.
  7. */
  8. class FormErrorHandler implements FormErrorHandlerInterface {
  9. /**
  10. * {@inheritdoc}
  11. */
  12. public function handleFormErrors(array &$form, FormStateInterface $form_state) {
  13. // After validation check if there are errors.
  14. if ($errors = $form_state->getErrors()) {
  15. // Display error messages for each element.
  16. $this->displayErrorMessages($form, $form_state);
  17. // Loop through and assign each element its errors.
  18. $this->setElementErrorsFromFormState($form, $form_state);
  19. }
  20. return $this;
  21. }
  22. /**
  23. * Loops through and displays all form errors.
  24. *
  25. * @param array $form
  26. * An associative array containing the structure of the form.
  27. * @param \Drupal\Core\Form\FormStateInterface $form_state
  28. * The current state of the form.
  29. */
  30. protected function displayErrorMessages(array $form, FormStateInterface $form_state) {
  31. $errors = $form_state->getErrors();
  32. // Loop through all form errors and set an error message.
  33. foreach ($errors as $error) {
  34. $this->drupalSetMessage($error, 'error');
  35. }
  36. }
  37. /**
  38. * Stores errors and a list of child element errors directly on each element.
  39. *
  40. * Grouping elements like containers, details, fieldgroups and fieldsets may
  41. * need error info from their child elements to be able to accessibly show
  42. * form error messages to a user. For example, a details element should be
  43. * opened when child elements have errors.
  44. *
  45. * Grouping example:
  46. * Assume you have a 'street' element somewhere in a form, which is displayed
  47. * in a details element 'address'. It might be:
  48. * @code
  49. * $form['street'] = [
  50. * '#type' => 'textfield',
  51. * '#title' => $this->t('Street'),
  52. * '#group' => 'address',
  53. * '#required' => TRUE,
  54. * ];
  55. * $form['address'] = [
  56. * '#type' => 'details',
  57. * '#title' => $this->t('Address'),
  58. * ];
  59. * @endcode
  60. *
  61. * When submitting an empty street field, the generated error is available to
  62. * the different render elements like so:
  63. * @code
  64. * // The street textfield element.
  65. * $element = [
  66. * '#errors' => {Drupal\Core\StringTranslation\TranslatableMarkup},
  67. * '#children_errors' => [],
  68. * ];
  69. * // The address detail element.
  70. * $element = [
  71. * '#errors' => null,
  72. * '#children_errors' => [
  73. * 'street' => {Drupal\Core\StringTranslation\TranslatableMarkup}
  74. * ],
  75. * ];
  76. * @endcode
  77. *
  78. * The list of child element errors of an element is an associative array. A
  79. * child element error is keyed with the #array_parents value of the
  80. * respective element. The key is formed by imploding this value with '][' as
  81. * glue. For example, a value ['contact_info', 'name'] becomes
  82. * 'contact_info][name'.
  83. *
  84. * @param array $form
  85. * An associative array containing a reference to the complete structure of
  86. * the form.
  87. * @param \Drupal\Core\Form\FormStateInterface $form_state
  88. * The current state of the form.
  89. * @param array $elements
  90. * An associative array containing the part of the form structure that will
  91. * be processed while traversing up the tree. For recursion only; leave
  92. * empty when calling this method.
  93. */
  94. protected function setElementErrorsFromFormState(array &$form, FormStateInterface $form_state, array &$elements = []) {
  95. // At the start of traversing up the form tree set the to be processed
  96. // elements to the complete form structure by reference so that we can
  97. // modify the original form. When processing grouped elements a reference to
  98. // the complete form is needed.
  99. if (empty($elements)) {
  100. $elements = &$form;
  101. }
  102. // Recurse through all element children.
  103. foreach (Element::children($elements) as $key) {
  104. if (!empty($elements[$key])) {
  105. // Get the child by reference so that we can update the original form.
  106. $child = &$elements[$key];
  107. // Call self to traverse up the form tree. The current element's child
  108. // contains the next elements to be processed.
  109. $this->setElementErrorsFromFormState($form, $form_state, $child);
  110. $children_errors = [];
  111. // Inherit all recorded "children errors" of the direct child.
  112. if (!empty($child['#children_errors'])) {
  113. $children_errors = $child['#children_errors'];
  114. }
  115. // Additionally store the errors of the direct child itself, keyed by
  116. // its parent elements structure.
  117. if (!empty($child['#errors'])) {
  118. $child_parents = implode('][', $child['#array_parents']);
  119. $children_errors[$child_parents] = $child['#errors'];
  120. }
  121. if (!empty($elements['#children_errors'])) {
  122. $elements['#children_errors'] += $children_errors;
  123. }
  124. else {
  125. $elements['#children_errors'] = $children_errors;
  126. }
  127. // If this direct child belongs to a group populate the grouping element
  128. // with the children errors.
  129. if (!empty($child['#group'])) {
  130. $parents = explode('][', $child['#group']);
  131. $group_element = NestedArray::getValue($form, $parents);
  132. if (isset($group_element['#children_errors'])) {
  133. $group_element['#children_errors'] = $group_element['#children_errors'] + $children_errors;
  134. }
  135. else {
  136. $group_element['#children_errors'] = $children_errors;
  137. }
  138. NestedArray::setValue($form, $parents, $group_element);
  139. }
  140. }
  141. }
  142. // Store the errors for this element on the element directly.
  143. $elements['#errors'] = $form_state->getError($elements);
  144. }
  145. /**
  146. * Wraps drupal_set_message().
  147. *
  148. * @codeCoverageIgnore
  149. */
  150. protected function drupalSetMessage($message = NULL, $type = 'status', $repeat = FALSE) {
  151. drupal_set_message($message, $type, $repeat);
  152. }
  153. }