Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
100.00% covered (success)
100.00%
70 / 70
100.00% covered (success)
100.00%
8 / 8
CRAP
100.00% covered (success)
100.00%
1 / 1
ElementValidatorService
100.00% covered (success)
100.00%
70 / 70
100.00% covered (success)
100.00%
8 / 8
31
100.00% covered (success)
100.00%
1 / 1
 __construct
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 validate
100.00% covered (success)
100.00%
17 / 17
100.00% covered (success)
100.00%
1 / 1
6
 resolveLabels
100.00% covered (success)
100.00%
5 / 5
100.00% covered (success)
100.00%
1 / 1
3
 resolveFilledComponents
100.00% covered (success)
100.00%
14 / 14
100.00% covered (success)
100.00%
1 / 1
7
 applyFamilyOrGivenLogic
100.00% covered (success)
100.00%
9 / 9
100.00% covered (success)
100.00%
1 / 1
6
 resolveMissingLabels
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
1
 setPartialInputErrors
100.00% covered (success)
100.00%
13 / 13
100.00% covered (success)
100.00%
1 / 1
4
 setRequiredErrors
100.00% covered (success)
100.00%
8 / 8
100.00% covered (success)
100.00%
1 / 1
3
1<?php
2
3declare(strict_types=1);
4
5namespace Drupal\name\Service;
6
7use Drupal\Core\Extension\ModuleHandlerInterface;
8use Drupal\Core\Form\FormStateInterface;
9use Drupal\Core\StringTranslation\StringTranslationTrait;
10use Drupal\Core\StringTranslation\TranslationInterface;
11use Drupal\name\Utility\NameComponents;
12
13/**
14 * Validates name element minimum components and required state.
15 *
16 * @internal
17 */
18class ElementValidatorService implements ElementValidatorInterface {
19
20  use StringTranslationTrait;
21
22  public function __construct(
23    private readonly ModuleHandlerInterface $moduleHandler,
24    TranslationInterface $string_translation,
25    private readonly NameComponentMetadataInterface $componentMetadata,
26  ) {
27    $this->setStringTranslation($string_translation);
28  }
29
30  /**
31   * Validates a name form element.
32   */
33  public function validate(array $element, FormStateInterface $form_state): array {
34    if (empty($element['#needs_validation'])) {
35      return $element;
36    }
37
38    $minimum_components = array_filter($element['#minimum_components']);
39    $labels             = $this->resolveLabels($element);
40    $item               = $element['#value'];
41    $empty              = NameComponents::isEmptyForValidation($item);
42    $item_components    = $this->resolveFilledComponents($element, $labels);
43    $item_components    = $this->applyFamilyOrGivenLogic($element, $labels, $item_components);
44    $missing_labels     = $this->resolveMissingLabels($minimum_components, $item_components, $labels);
45    $is_inline          = (bool) $this->moduleHandler->moduleExists('inline_form_errors');
46
47    $has_partial_input = (!$empty && !empty($missing_labels));
48    if ($has_partial_input) {
49      $this->setPartialInputErrors($element, $form_state, $missing_labels, $is_inline);
50    }
51
52    $is_required_and_empty = ($empty && $element['#required']);
53    if ($is_required_and_empty) {
54      $this->setRequiredErrors($element, $form_state, $missing_labels, $is_inline);
55    }
56    return $element;
57  }
58
59  /**
60   * Resolves validation labels for enabled components.
61   */
62  private function resolveLabels(array $element): array {
63    $labels = [];
64    foreach ($element['#components'] as $key => $component) {
65      if (!isset($component['exclude'])) {
66        $labels[$key] = $component['title'];
67      }
68    }
69
70    return $labels;
71  }
72
73  /**
74   * Resolves components containing user-entered values.
75   */
76  private function resolveFilledComponents(array $element, array $labels): array {
77    $item = $element['#value'];
78    $item_components = [];
79
80    $translation_keys = array_keys($this->componentMetadata->getTranslations());
81    foreach ($translation_keys as $key) {
82      $component_is_absent = (!isset($labels[$key]) && !isset($item[$key]));
83      if ($component_is_absent) {
84        continue;
85      }
86
87      $value = $item[$key] ?? NULL;
88
89      $is_select = (($element['#components'][$key]['type'] ?? NULL) === 'select');
90      $is_none_selection = ($is_select && $value === '_none');
91      if ($is_none_selection) {
92        $value = '';
93      }
94
95      if (!empty($value)) {
96        $item_components[$key] = 1;
97      }
98    }
99
100    return $item_components;
101  }
102
103  /**
104   * Applies the family-or-given shortcut when it is configured.
105   */
106  private function applyFamilyOrGivenLogic(
107    array $element,
108    array $labels,
109    array $item_components,
110  ): array {
111    $item = $element['#value'];
112    if (!empty($element['#allow_family_or_given'])) {
113      $has_given_and_family = (isset($labels['given']) && isset($labels['family']));
114      if ($has_given_and_family) {
115        $has_given_or_family = (!empty($item['given']) || !empty($item['family']));
116        if ($has_given_or_family) {
117          $item_components['given'] = 1;
118          $item_components['family'] = 1;
119        }
120      }
121    }
122
123    return $item_components;
124  }
125
126  /**
127   * Resolves enabled labels for missing minimum components.
128   */
129  private function resolveMissingLabels(
130    array $minimum_components,
131    array $item_components,
132    array $labels,
133  ): array {
134    $missing_components = array_diff(array_keys($minimum_components), array_keys($item_components));
135    $missing_components = array_combine($missing_components, $missing_components);
136
137    return array_intersect_key($labels, $missing_components);
138  }
139
140  /**
141   * Sets validation errors for partial name input.
142   */
143  private function setPartialInputErrors(
144    array $element,
145    FormStateInterface $form_state,
146    array $missing_labels,
147    bool $is_inline,
148  ): void {
149    if ($is_inline) {
150      foreach ($missing_labels as $key => $label) {
151        $form_state->setError($element[$key], $this->t('@name requires <em>@components</em>.', [
152          '@name' => $element['#title'],
153          '@components' => $label,
154        ]));
155      }
156      return;
157    }
158
159    $form_state->setError($element[key($missing_labels)], $this->t('@name requires <em>@components</em>.', [
160      '@name' => $element['#title'],
161      '@components' => implode(', ', $missing_labels),
162    ]));
163
164    foreach ($missing_labels as $key => $label) {
165      $form_state->setError($element[$key]);
166    }
167  }
168
169  /**
170   * Sets validation errors for required name input.
171   */
172  private function setRequiredErrors(
173    array $element,
174    FormStateInterface $form_state,
175    array $missing_labels,
176    bool $is_inline,
177  ): void {
178    if ($is_inline) {
179      foreach ($missing_labels as $key => $label) {
180        $form_state->setError($element[$key], $this->t('@name requires <em>@components</em>.', [
181          '@name' => $element['#title'],
182          '@components' => $label,
183        ]));
184      }
185      return;
186    }
187
188    $form_state->setError($element, $this->t('@name field is required.', ['@name' => $element['#title']]));
189  }
190
191}