Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
100.00% covered (success)
100.00%
209 / 209
100.00% covered (success)
100.00%
14 / 14
CRAP
100.00% covered (success)
100.00%
1 / 1
Name
100.00% covered (success)
100.00%
209 / 209
100.00% covered (success)
100.00%
14 / 14
44
100.00% covered (success)
100.00%
1 / 1
 __construct
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 create
100.00% covered (success)
100.00%
6 / 6
100.00% covered (success)
100.00%
1 / 1
1
 trustedCallbacks
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getInfo
100.00% covered (success)
100.00%
76 / 76
100.00% covered (success)
100.00%
1 / 1
1
 valueCallback
100.00% covered (success)
100.00%
17 / 17
100.00% covered (success)
100.00%
1 / 1
5
 process
100.00% covered (success)
100.00%
29 / 29
100.00% covered (success)
100.00%
1 / 1
6
 renderComponent
100.00% covered (success)
100.00%
6 / 6
100.00% covered (success)
100.00%
1 / 1
1
 componentDescriptionAfterBuildLabelAlter
100.00% covered (success)
100.00%
8 / 8
100.00% covered (success)
100.00%
1 / 1
4
 validateElement
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
2
 validateIsEmpty
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 preRender
100.00% covered (success)
100.00%
25 / 25
100.00% covered (success)
100.00%
1 / 1
6
 applyDetailsWrapper
100.00% covered (success)
100.00%
17 / 17
100.00% covered (success)
100.00%
1 / 1
6
 resolveWidgetLayout
100.00% covered (success)
100.00%
15 / 15
100.00% covered (success)
100.00%
1 / 1
7
 getComponentTranslations
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
2
1<?php
2
3declare(strict_types=1);
4
5namespace Drupal\name\Element;
6
7use Drupal\Component\Utility\Html;
8use Drupal\Core\Field\FieldTypePluginManagerInterface;
9use Drupal\Core\Form\FormStateInterface;
10use Drupal\Core\Plugin\ContainerFactoryPluginInterface;
11use Drupal\Core\Render\Attribute\RenderElement;
12use Drupal\Core\Render\Element\FormElementBase;
13use Drupal\Core\Security\TrustedCallbackInterface;
14use Drupal\Core\Template\Attribute;
15use Drupal\name\Service\WidgetLayoutInterface;
16use Drupal\name\Utility\ComponentBuilder;
17use Drupal\name\Utility\NameComponents;
18use Symfony\Component\DependencyInjection\ContainerInterface;
19
20/**
21 * Provides a name render element.
22 */
23#[RenderElement('name')]
24class Name extends FormElementBase implements ContainerFactoryPluginInterface, TrustedCallbackInterface {
25
26  /**
27   * Field type plugin manager.
28   */
29  protected FieldTypePluginManagerInterface $fieldTypeManager;
30
31  /**
32   * Constructs a name form element plugin.
33   *
34   * @param array $configuration
35   *   A configuration array containing plugin instance information.
36   * @param string $plugin_id
37   *   The plugin ID for the plugin instance.
38   * @param mixed $plugin_definition
39   *   The plugin implementation definition.
40   * @param \Drupal\Core\Field\FieldTypePluginManagerInterface $field_type_manager
41   *   The field type plugin manager service.
42   */
43  public function __construct(array $configuration, $plugin_id, $plugin_definition, FieldTypePluginManagerInterface $field_type_manager) {
44    parent::__construct($configuration, $plugin_id, $plugin_definition);
45    $this->fieldTypeManager = $field_type_manager;
46  }
47
48  /**
49   * {@inheritdoc}
50   */
51  public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition): static {
52    return new static(
53      $configuration,
54      $plugin_id,
55      $plugin_definition,
56      $container->get('plugin.manager.field.field_type')
57    );
58  }
59
60  /**
61   * {@inheritdoc}
62   */
63  public static function trustedCallbacks() {
64    return ['preRender'];
65  }
66
67  /**
68   * {@inheritdoc}
69   */
70  public function getInfo() {
71    $parts = static::getComponentTranslations();
72    $field_settings = $this->fieldTypeManager->getDefaultFieldSettings('name');
73
74    return [
75      '#input' => TRUE,
76      '#process' => [[__CLASS__, 'process']],
77      '#pre_render' => [[__CLASS__, 'preRender']],
78      '#element_validate' => [[__CLASS__, 'validateElement']],
79      '#theme_wrappers' => ['fieldset'],
80      '#wrapper_type' => 'fieldset',
81      '#open' => FALSE,
82      '#summary_attributes' => [],
83      '#show_component_required_marker' => 0,
84      '#flag_required_input' => TRUE,
85      '#default_value' => [
86        'title' => '',
87        'given' => '',
88        'middle' => '',
89        'family' => '',
90        'generational' => '',
91        'credentials' => '',
92      ],
93      '#minimum_components' => $field_settings['minimum_components'],
94      '#allow_family_or_given' => $field_settings['allow_family_or_given'],
95      '#components' => [
96        'title' => [
97          'type' => $field_settings['field_type']['title'],
98          'title' => $parts['title'],
99          'title_display' => 'description',
100          'size' => $field_settings['size']['title'],
101          'maxlength' => $field_settings['max_length']['title'],
102          'options' => $field_settings['title_options'],
103          'autocomplete' => FALSE,
104        ],
105        'given' => [
106          'type' => 'textfield',
107          'title' => $parts['given'],
108          'title_display' => 'description',
109          'size' => $field_settings['size']['given'],
110          'maxlength' => $field_settings['max_length']['given'],
111          'autocomplete' => FALSE,
112        ],
113        'middle' => [
114          'type' => 'textfield',
115          'title' => $parts['middle'],
116          'title_display' => 'description',
117          'size' => $field_settings['size']['middle'],
118          'maxlength' => $field_settings['max_length']['middle'],
119          'autocomplete' => FALSE,
120        ],
121        'family' => [
122          'type' => 'textfield',
123          'title' => $parts['family'],
124          'title_display' => 'description',
125          'size' => $field_settings['size']['family'],
126          'maxlength' => $field_settings['max_length']['family'],
127          'autocomplete' => FALSE,
128        ],
129        'generational' => [
130          'type' => $field_settings['field_type']['generational'],
131          'title' => $parts['generational'],
132          'title_display' => 'description',
133          'size' => $field_settings['size']['generational'],
134          'maxlength' => $field_settings['max_length']['generational'],
135          'options' => $field_settings['generational_options'],
136          'autocomplete' => FALSE,
137        ],
138        'credentials' => [
139          'type' => 'textfield',
140          'title' => $parts['credentials'],
141          'title_display' => 'description',
142          'size' => $field_settings['size']['credentials'],
143          'maxlength' => $field_settings['max_length']['credentials'],
144          'autocomplete' => FALSE,
145        ],
146      ],
147    ];
148  }
149
150  /**
151   * {@inheritdoc}
152   */
153  public static function valueCallback(&$element, $input, FormStateInterface $form_state) {
154    $value = [
155      'title' => '',
156      'given' => '',
157      'middle' => '',
158      'family' => '',
159      'generational' => '',
160      'credentials' => '',
161    ];
162    if ($input === FALSE) {
163      $element += ['#default_value' => []];
164      return $element['#default_value'] + $value;
165    }
166    $allowed_keys = array_keys($value);
167    foreach ($allowed_keys as $allowed_key) {
168      $has_scalar_input = (isset($input[$allowed_key]) && is_scalar($input[$allowed_key]));
169      if ($has_scalar_input) {
170        $value[$allowed_key] = (string) $input[$allowed_key];
171      }
172    }
173    return $value;
174  }
175
176  /**
177   * Process callback: expands child component elements.
178   */
179  public static function process($element) {
180    $element['#tree'] = TRUE;
181    if (empty($element['#value'])) {
182      $element['#value'] = [];
183    }
184
185    $parts = static::getComponentTranslations();
186    $components = $element['#components'];
187    $min_components = (array) $element['#minimum_components'];
188
189    $component_keys = array_keys($parts);
190    foreach ($component_keys as $component) {
191      if (isset($components[$component]['exclude'])) {
192        continue;
193      }
194
195      $element[$component] = static::renderComponent(
196        $components,
197        $component,
198        $element,
199        isset($min_components[$component]),
200      );
201      $attributes = [
202        'class' => [
203          'name-component-wrapper',
204          'name-' . $component . '-wrapper',
205        ],
206      ];
207      $should_break_credentials = (
208        $component == 'credentials' && empty($element['#credentials_inline'])
209      );
210      if ($should_break_credentials) {
211        $attributes['class'][] = 'name-component-break';
212      }
213      $element[$component]['#prefix'] = '<div' . new Attribute($attributes) . '>';
214      $element[$component]['#suffix'] = '</div>';
215    }
216
217    return $element;
218  }
219
220  /**
221   * Builds a single component sub-element for process().
222   *
223   * @param array $components
224   *   Core properties for all components.
225   * @param string $component_key
226   *   The component key of the component that is being rendered.
227   * @param array $base_element
228   *   Base FAPI element that makes up a name element.
229   * @param bool $core
230   *   Whether the component is required as part of a valid name.
231   *
232   * @return array
233   *   The constructed component FAPI structure for a name element.
234   */
235  public static function renderComponent(array $components, $component_key, array $base_element, $core) {
236    return ComponentBuilder::renderComponent(
237      $components,
238      (string) $component_key,
239      $base_element,
240      (bool) $core,
241    );
242  }
243
244  /**
245   * After-build callback: sets #for on description label render arrays.
246   */
247  public static function componentDescriptionAfterBuildLabelAlter(array $element) {
248    $has_description_label = (
249      !empty($element['#description'])
250      && !empty($element['#id'])
251      && is_array($element['#description'])
252    );
253    if ($has_description_label) {
254      $element['#description']['#for'] = $element['#id'];
255    }
256    return $element;
257  }
258
259  /**
260   * Element validate entrypoint; delegates to the element validator service.
261   */
262  public static function validateElement($element, FormStateInterface &$form_state) {
263    $validator = \Drupal::getContainer()
264      ->get('name.element_validator', ContainerInterface::NULL_ON_INVALID_REFERENCE);
265    return $validator ? $validator->validate($element, $form_state) : $element;
266  }
267
268  /**
269   * Whether a name value array is empty for validation purposes.
270   */
271  public static function validateIsEmpty(array $item): bool {
272    return NameComponents::isEmptyForValidation($item);
273  }
274
275  /**
276   * Pre-render: builds wrapper layout and moves children under _name.
277   */
278  public static function preRender($element) {
279    static::applyDetailsWrapper($element);
280
281    $layouts_service = \Drupal::getContainer()
282      ->get('name.widget_layouts', ContainerInterface::NULL_ON_INVALID_REFERENCE);
283    $layout = static::resolveWidgetLayout(
284      $layouts_service instanceof WidgetLayoutInterface ? $layouts_service : NULL,
285      $element['#widget_layout'] ?? NULL,
286    );
287
288    if (!empty($layout['library'])) {
289      $element['#attached']['library'] = array_merge(
290        $element['#attached']['library'] ?? [],
291        $layout['library'],
292      );
293    }
294    $attributes = new Attribute($layout['wrapper_attributes']);
295    $element['_name'] = [
296      '#prefix' => '<div' . $attributes . '>',
297      '#suffix' => '</div>',
298    ];
299
300    $translation_keys = array_keys(static::getComponentTranslations());
301    foreach ($translation_keys as $key) {
302      if (isset($element[$key])) {
303        $element['_name'][$key] = $element[$key];
304        unset($element[$key]);
305      }
306    }
307
308    if (!empty($element['#component_layout'])) {
309      NameComponents::applyLayout($element['_name'], $element['#component_layout']);
310    }
311
312    return $element;
313  }
314
315  /**
316   * Applies the configured wrapper type to the element.
317   *
318   * @param array $element
319   *   The render element being prepared.
320   */
321  public static function applyDetailsWrapper(array &$element): void {
322    $wrapper_type = $element['#wrapper_type'] ?? 'fieldset';
323    if (!in_array($wrapper_type, ['container', 'details', 'fieldset'], TRUE)) {
324      $wrapper_type = 'fieldset';
325    }
326
327    $element['#theme_wrappers'] = [$wrapper_type];
328    if ($wrapper_type !== 'details') {
329      return;
330    }
331
332    $title = isset($element['#title'])
333      ? Html::escape((string) $element['#title'])
334      : '';
335    $required_classes = !empty($element['#required'])
336      ? ' class="js-form-required form-required"'
337      : '';
338    $open = !empty($element['#open']) ? ' open' : '';
339    $element['#theme_wrappers'] = ['container'];
340    $element['#prefix'] = '<details' . $open . '><summary'
341      . $required_classes . '>' . $title . '</summary>';
342    $element['#suffix'] = '</details>';
343  }
344
345  /**
346   * Resolves a widget layout definition with required defaults.
347   *
348   * @param \Drupal\name\Service\WidgetLayoutInterface|null $service
349   *   The widget layout service, if available.
350   * @param string|null $widget_layout
351   *   The requested widget layout key.
352   *
353   * @return array
354   *   A normalized layout definition.
355   */
356  public static function resolveWidgetLayout(
357    ?WidgetLayoutInterface $service,
358    ?string $widget_layout,
359  ): array {
360    $default_layout = [
361      'library' => [],
362      'wrapper_attributes' => [
363        'class' => ['name-widget-wrapper'],
364      ],
365    ];
366    $layouts = $service ? $service->getLayouts() : [];
367    $layout = $default_layout;
368    if ($layouts !== []) {
369      $layout = $layouts['stacked'] ?? reset($layouts) ?: $default_layout;
370    }
371
372    $has_requested_layout = $widget_layout !== NULL && isset($layouts[$widget_layout]);
373    if ($has_requested_layout) {
374      $layout = $layouts[$widget_layout];
375    }
376
377    $layout += [
378      'library' => [],
379      'wrapper_attributes' => [],
380    ];
381    $layout['wrapper_attributes'] += ['class' => []];
382    if (!in_array('name-widget-wrapper', $layout['wrapper_attributes']['class'], TRUE)) {
383      $layout['wrapper_attributes']['class'][] = 'name-widget-wrapper';
384    }
385
386    return $layout;
387  }
388
389  /**
390   * Loads translated component labels from the metadata service.
391   *
392   * @return array<string, \Drupal\Core\StringTranslation\TranslatableMarkup|string>
393   *   Keyed labels.
394   */
395  protected static function getComponentTranslations(): array {
396    $metadata = \Drupal::getContainer()
397      ->get('name.component_metadata', ContainerInterface::NULL_ON_INVALID_REFERENCE);
398    return $metadata ? $metadata->getTranslations() : [];
399  }
400
401}