Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
100.00% covered (success)
100.00%
158 / 158
100.00% covered (success)
100.00%
12 / 12
CRAP
100.00% covered (success)
100.00%
1 / 1
NameWidget
100.00% covered (success)
100.00%
158 / 158
100.00% covered (success)
100.00%
12 / 12
36
100.00% covered (success)
100.00%
1 / 1
 __construct
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
1
 create
100.00% covered (success)
100.00%
9 / 9
100.00% covered (success)
100.00%
1 / 1
1
 formElement
100.00% covered (success)
100.00%
6 / 6
100.00% covered (success)
100.00%
1 / 1
1
 buildFormElementDefaults
100.00% covered (success)
100.00%
15 / 15
100.00% covered (success)
100.00%
1 / 1
6
 applyFieldTitleDisplay
100.00% covered (success)
100.00%
7 / 7
100.00% covered (success)
100.00%
1 / 1
3
 applyComponentDefinitions
100.00% covered (success)
100.00%
9 / 9
100.00% covered (success)
100.00%
1 / 1
3
 resolveSettings
100.00% covered (success)
100.00%
9 / 9
100.00% covered (success)
100.00%
1 / 1
3
 buildComponentProperties
100.00% covered (success)
100.00%
26 / 26
100.00% covered (success)
100.00%
1 / 1
8
 massageFormValues
100.00% covered (success)
100.00%
12 / 12
100.00% covered (success)
100.00%
1 / 1
6
 defaultSettings
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
1 / 1
1
 settingsForm
100.00% covered (success)
100.00%
43 / 43
100.00% covered (success)
100.00%
1 / 1
1
 settingsSummary
100.00% covered (success)
100.00%
15 / 15
100.00% covered (success)
100.00%
1 / 1
2
1<?php
2
3declare(strict_types=1);
4
5namespace Drupal\name\Plugin\Field\FieldWidget;
6
7use Drupal\Component\Utility\Html;
8use Drupal\Core\Field\Attribute\FieldWidget;
9use Drupal\Core\Field\FieldDefinitionInterface;
10use Drupal\Core\Field\FieldItemListInterface;
11use Drupal\Core\Field\WidgetBase;
12use Drupal\Core\Form\FormStateInterface;
13use Drupal\Core\Plugin\ContainerFactoryPluginInterface;
14use Drupal\Core\Security\TrustedCallbackInterface;
15use Drupal\Core\StringTranslation\TranslatableMarkup;
16use Drupal\name\Service\NameComponentMetadataInterface;
17use Drupal\name\Service\NameOptionInterface;
18use Drupal\name\Traits\NameFormDisplaySettingsTrait;
19use Drupal\name\Traits\NameFormSettingsHelperTrait;
20use Symfony\Component\DependencyInjection\ContainerInterface;
21
22/**
23 * Plugin implementation of the 'name' widget.
24 */
25#[FieldWidget(
26  id: "name_default",
27  label: new TranslatableMarkup("Name components"),
28  field_types: ["name"]
29)]
30class NameWidget extends WidgetBase implements ContainerFactoryPluginInterface, TrustedCallbackInterface {
31
32  use NameFormDisplaySettingsTrait;
33  use NameFormSettingsHelperTrait;
34
35  /**
36   * Name options provider service.
37   *
38   * @var \Drupal\name\Service\NameOptionInterface
39   */
40  protected $optionsProvider;
41
42  /**
43   * Translated component labels and related metadata.
44   *
45   * @var \Drupal\name\Service\NameComponentMetadataInterface
46   */
47  protected NameComponentMetadataInterface $componentMetadata;
48
49  /**
50   * Constructs a NameWidget object.
51   *
52   * @param string $plugin_id
53   *   The plugin_id for the widget.
54   * @param mixed $plugin_definition
55   *   The plugin implementation definition.
56   * @param \Drupal\Core\Field\FieldDefinitionInterface $field_definition
57   *   The definition of the field to which the widget is associated.
58   * @param array $settings
59   *   The widget settings.
60   * @param array $third_party_settings
61   *   Any third party settings.
62   * @param \Drupal\name\Service\NameOptionInterface|null $options_provider
63   *   Name options provider service.
64   * @param \Drupal\name\Service\NameComponentMetadataInterface|null $component_metadata
65   *   Component label metadata.
66   */
67  public function __construct($plugin_id, $plugin_definition, FieldDefinitionInterface $field_definition, array $settings, array $third_party_settings, ?NameOptionInterface $options_provider, ?NameComponentMetadataInterface $component_metadata) {
68    parent::__construct($plugin_id, $plugin_definition, $field_definition, $settings, $third_party_settings);
69    // @phpstan-ignore-next-line
70    $this->optionsProvider = $options_provider ?? \Drupal::service('name.options_provider');
71    // @phpstan-ignore-next-line
72    $this->componentMetadata = $component_metadata ?? \Drupal::service('name.component_metadata');
73  }
74
75  /**
76   * {@inheritdoc}
77   */
78  public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition) {
79    return new static(
80      $plugin_id,
81      $plugin_definition,
82      $configuration['field_definition'],
83      $configuration['settings'],
84      $configuration['third_party_settings'],
85      $container->get('name.options_provider', ContainerInterface::NULL_ON_INVALID_REFERENCE),
86      $container->get('name.component_metadata', ContainerInterface::NULL_ON_INVALID_REFERENCE)
87    );
88  }
89
90  /**
91   * {@inheritdoc}
92   */
93  public function formElement(FieldItemListInterface $items, $delta, array $element, array &$form, FormStateInterface $form_state) {
94    $settings = $this->resolveSettings($form_state);
95    $widget_settings = $this->getSettings();
96
97    $element += $this->buildFormElementDefaults($items, $delta, $settings, $widget_settings);
98    $element = $this->applyFieldTitleDisplay($element, $settings);
99    $element['#components'] = $this->applyComponentDefinitions($settings);
100
101    return $element;
102  }
103
104  /**
105   * Builds default render array properties for the name form element.
106   *
107   * @param \Drupal\Core\Field\FieldItemListInterface $items
108   *   The field items.
109   * @param int $delta
110   *   The item delta.
111   * @param array $settings
112   *   Active widget settings.
113   * @param array $widget_settings
114   *   Widget plugin settings.
115   *
116   * @return array
117   *   Default element properties.
118   */
119  private function buildFormElementDefaults(
120    FieldItemListInterface $items,
121    int $delta,
122    array $settings,
123    array $widget_settings,
124  ): array {
125    $element = [
126      '#type' => 'name',
127      '#title' => $this->fieldDefinition->getLabel(),
128      '#minimum_components' => array_filter($settings['minimum_components']),
129      '#allow_family_or_given' => !empty($settings['allow_family_or_given']),
130      '#default_value' => isset($items[$delta]) ? $items[$delta]->getValue() : NULL,
131      '#field' => $this,
132      '#credentials_inline' => empty($settings['credentials_inline']) ? 0 : 1,
133      '#widget_layout' => empty($settings['widget_layout']) ? 'stacked' : $settings['widget_layout'],
134      '#wrapper_type' => empty($widget_settings['wrapper_type']) ? 'fieldset' : $widget_settings['wrapper_type'],
135      '#component_layout' => empty($settings['component_layout']) ? 'default' : $settings['component_layout'],
136      '#show_component_required_marker' => !empty($settings['show_component_required_marker']),
137      '#flag_required_input' => !empty($settings['flag_required_input']),
138    ];
139
140    return $element;
141  }
142
143  /**
144   * Applies field-level title display when the widget defaults to "before".
145   *
146   * @param array $element
147   *   The form element render array.
148   * @param array $settings
149   *   Active widget settings.
150   *
151   * @return array
152   *   The element with title display updated when applicable.
153   */
154  private function applyFieldTitleDisplay(array $element, array $settings): array {
155    $may_override_title = (
156      !empty($settings['field_title_display'])
157      && ($element['#title_display'] ?? NULL) === 'before'
158    );
159    if ($may_override_title) {
160      $element['#title_display'] = $settings['field_title_display'];
161    }
162
163    return $element;
164  }
165
166  /**
167   * Builds per-component definitions for the name form element.
168   *
169   * @param array $settings
170   *   Active widget settings.
171   *
172   * @return array
173   *   Component render definitions keyed by component machine name.
174   */
175  private function applyComponentDefinitions(array $settings): array {
176    $components = [];
177    $active = array_filter($settings['components']);
178    $translation_keys = array_keys($this->componentMetadata->getTranslations());
179    foreach ($translation_keys as $key) {
180      if (!isset($active[$key])) {
181        $components[$key]['exclude'] = TRUE;
182        continue;
183      }
184      $components[$key] = $this->buildComponentProperties($key, $settings);
185    }
186
187    return $components;
188  }
189
190  /**
191   * Resolves the active field widget settings.
192   *
193   * @param \Drupal\Core\Form\FormStateInterface $form_state
194   *   The current form state.
195   *
196   * @return array
197   *   The active widget settings.
198   */
199  protected function resolveSettings(FormStateInterface $form_state): array {
200    $widget_settings = $this->getSettings();
201    $field_settings = $this->getFieldSettings();
202    $should_use_widget_settings = (
203      !empty($widget_settings['override_field_settings'])
204      && !$this->isDefaultValueWidget($form_state)
205    );
206    if ($should_use_widget_settings) {
207      return $widget_settings + $field_settings;
208    }
209
210    return $field_settings;
211  }
212
213  /**
214   * Builds the render properties for a single name component.
215   *
216   * @param string $key
217   *   The component key.
218   * @param array $settings
219   *   The active widget settings.
220   *
221   * @return array
222   *   The component render properties.
223   */
224  private function buildComponentProperties(string $key, array $settings): array {
225    $component = [
226      'type' => 'textfield',
227      'title' => Html::escape((string) $settings['labels'][$key]),
228      'title_display' => $settings['title_display'][$key] ?? 'description',
229      'size' => !empty($settings['size'][$key]) ? $settings['size'][$key] : 60,
230      'maxlength' => !empty($settings['max_length'][$key]) ? $settings['max_length'][$key] : 255,
231    ];
232
233    // Provides backwards compatibility with Drupal 6 modules.
234    $field_type = ($key === 'title' || $key === 'generational') ? 'select' : 'text';
235    $field_type = $settings['field_type'][$key] ?? ($settings[$key . '_field'] ?? $field_type);
236
237    if ($field_type === 'select') {
238      $component['type'] = 'select';
239      $component['size'] = 1;
240      $component['options'] = $this->optionsProvider->getOptions($this->fieldDefinition, $key);
241    }
242    elseif ($field_type === 'autocomplete') {
243      $sources = array_filter($settings['autocomplete_source'][$key] ?? []);
244      if (!empty($sources)) {
245        $component['autocomplete'] = [
246          '#autocomplete_route_name' => 'name.autocomplete',
247          '#autocomplete_route_parameters' => [
248            'field_name' => $this->fieldDefinition->getName(),
249            'entity_type' => $this->fieldDefinition->getTargetEntityTypeId(),
250            'bundle' => $this->fieldDefinition->getTargetBundle(),
251            'component' => $key,
252          ],
253        ];
254      }
255    }
256
257    return $component;
258  }
259
260  /**
261   * {@inheritdoc}
262   */
263  public function massageFormValues(array $values, array $form, FormStateInterface $form_state) {
264    $values = parent::massageFormValues($values, $form, $form_state);
265
266    // Get fields that use selection.
267    $selection_fields = array_keys($this->fieldDefinition->getSettings()['field_type'], 'select', TRUE);
268
269    $new_values = [];
270    foreach ($values as $item) {
271      // For all selection fields, replace '_none' with an empty string.
272      foreach ($selection_fields as $field_name) {
273        $is_none_selection = (isset($item[$field_name]) && $item[$field_name] == '_none');
274        if ($is_none_selection) {
275          $item[$field_name] = '';
276        }
277      }
278
279      $value = implode('', array_intersect_key($item, $this->componentMetadata->getTranslations()));
280      if (strlen($value)) {
281        $new_values[] = $item;
282      }
283    }
284    return $new_values;
285  }
286
287  /**
288   * {@inheritdoc}
289   */
290  public static function defaultSettings() {
291    $settings = self::getDefaultNameFormDisplaySettings();
292    $settings['override_field_settings'] = FALSE;
293    $settings['wrapper_type'] = 'fieldset';
294    return $settings + parent::defaultSettings();
295  }
296
297  /**
298   * {@inheritdoc}
299   */
300  public function settingsForm(array $form, FormStateInterface $form_state) {
301    $element = parent::settingsForm($form, $form_state);
302    $settings = $this->getSettings();
303
304    $element['override_field_settings'] = [
305      '#type' => 'checkbox',
306      '#title' => $this->t('Override shared field settings'),
307      '#default_value' => $this->getSetting('override_field_settings'),
308      '#table_group' => 'above',
309      '#weight' => -100,
310    ];
311    $element['wrapper_type'] = [
312      '#type' => 'radios',
313      '#title' => $this->t('Wrapper type'),
314      '#default_value' => $settings['wrapper_type'] ?? 'fieldset',
315      '#options' => [
316        'container' => $this->t('Container (invisible)'),
317        'details' => $this->t('Details (collapsible)'),
318        'fieldset' => $this->t('Fieldset (non-collapsible)'),
319      ],
320      '#table_group' => 'above',
321      '#weight' => -99,
322    ];
323
324    $element += $this->getDefaultNameFormDisplaySettingsForm($settings);
325
326    // Remove inaccessible name components as defined in the field settings.
327    $field_settings = $this->getFieldSettings();
328    $components = array_keys(array_filter($field_settings['components']));
329    $components = array_combine($components, $components);
330    $element['#excluded_components'] = array_diff_key($this->componentMetadata->getTranslations(), $components);
331    $element['#pre_render'][] = [$this, 'fieldSettingsFormPreRender'];
332    $element['widget_layout']['#states'] = [
333      'visible' => [
334        ':input[name$="[override_field_settings]"]' => [
335          'checked' => TRUE,
336        ],
337      ],
338    ];
339    $element['field_title_display']['#states'] = [
340      'visible' => [
341        ':input[name$="[override_field_settings]"]' => [
342          'checked' => TRUE,
343        ],
344      ],
345    ];
346    $element['name_settings']['#states'] = $element['widget_layout']['#states'];
347
348    return $element;
349  }
350
351  /**
352   * {@inheritdoc}
353   */
354  public function settingsSummary() {
355    $summary = parent::settingsSummary();
356    $widget_settings = $this->getSettings();
357    $wrapper_options = [
358      'container' => $this->t('Container (invisible)'),
359      'details' => $this->t('Details (collapsible)'),
360      'fieldset' => $this->t('Fieldset (non-collapsible)'),
361    ];
362    $summary_label = empty($widget_settings['override_field_settings'])
363      ? $this->t('Using shared settings')
364      : $this->t('Overridden settings');
365    array_unshift($summary, $summary_label);
366    $wrapper_type = $widget_settings['wrapper_type'] ?? 'fieldset';
367    $wrapper_label = $wrapper_options[$wrapper_type] ?? $wrapper_options['fieldset'];
368    $summary[] = $this->t('Wrapper type: @wrapper_type', ['@wrapper_type' => $wrapper_label]);
369
370    return $summary;
371  }
372
373}