Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
100.00% covered (success)
100.00%
139 / 139
100.00% covered (success)
100.00%
13 / 13
CRAP
100.00% covered (success)
100.00%
1 / 1
NameItem
100.00% covered (success)
100.00%
139 / 139
100.00% covered (success)
100.00%
13 / 13
37
100.00% covered (success)
100.00%
1 / 1
 __sleep
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
1
 schema
100.00% covered (success)
100.00%
14 / 14
100.00% covered (success)
100.00%
1 / 1
2
 defaultFieldSettings
100.00% covered (success)
100.00%
5 / 5
100.00% covered (success)
100.00%
1 / 1
1
 propertyDefinitions
100.00% covered (success)
100.00%
14 / 14
100.00% covered (success)
100.00%
1 / 1
1
 mainPropertyName
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 hasPopulatedProperty
100.00% covered (success)
100.00%
8 / 8
100.00% covered (success)
100.00%
1 / 1
4
 hasMeaningfulRawValues
100.00% covered (success)
100.00%
7 / 7
100.00% covered (success)
100.00%
1 / 1
5
 isEmpty
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
2
 filteredArray
100.00% covered (success)
100.00%
9 / 9
100.00% covered (success)
100.00%
1 / 1
4
 activeComponents
100.00% covered (success)
100.00%
9 / 9
100.00% covered (success)
100.00%
1 / 1
5
 fieldSettingsForm
100.00% covered (success)
100.00%
44 / 44
100.00% covered (success)
100.00%
1 / 1
4
 validateUserPreferred
100.00% covered (success)
100.00%
18 / 18
100.00% covered (success)
100.00%
1 / 1
5
 generateSampleValue
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
1 / 1
2
1<?php
2
3declare(strict_types=1);
4
5namespace Drupal\name\Plugin\Field\FieldType;
6
7use Drupal\Core\Cache\Cache;
8use Drupal\Core\Field\Attribute\FieldType;
9use Drupal\Core\Field\FieldDefinitionInterface;
10use Drupal\Core\Field\FieldItemBase;
11use Drupal\Core\Field\FieldStorageDefinitionInterface;
12use Drupal\Core\Form\FormStateInterface;
13use Drupal\Core\Link;
14use Drupal\Core\Security\TrustedCallbackInterface;
15use Drupal\Core\StringTranslation\TranslatableMarkup;
16use Drupal\Core\TypedData\DataDefinition;
17use Drupal\Core\Url;
18use Drupal\name\Traits\NameAdditionalPreferredTrait;
19use Drupal\name\Traits\NameFieldSettingsTrait;
20use Drupal\name\Traits\NameFormDisplaySettingsTrait;
21use Drupal\name\Traits\NameFormSettingsHelperTrait;
22use Drupal\name\Utility\NameComponents;
23use Symfony\Component\DependencyInjection\ContainerInterface;
24
25/**
26 * Plugin implementation of the 'name' field type.
27 *
28 * Majority of the settings handling is delegated to the traits so that these
29 * can be reused.
30 */
31#[FieldType(
32  id: "name",
33  label: new TranslatableMarkup("Name"),
34  description: new TranslatableMarkup("Stores real name."),
35  default_widget: "name_default",
36  default_formatter: "name_default"
37)]
38class NameItem extends FieldItemBase implements TrustedCallbackInterface {
39
40  use NameFieldSettingsTrait;
41  use NameFormDisplaySettingsTrait;
42  use NameFormSettingsHelperTrait;
43  use NameAdditionalPreferredTrait;
44
45  /**
46   * {@inheritdoc}
47   */
48  public function __sleep(): array {
49    $keys = parent::__sleep();
50    unset($keys[array_search('values', $keys)]);
51
52    return $keys;
53  }
54
55  /**
56   * Definition of name field components.
57   *
58   * @var array
59   */
60  protected static $components = [
61    'title',
62    'given',
63    'middle',
64    'family',
65    'generational',
66    'credentials',
67  ];
68
69  /**
70   * {@inheritdoc}
71   */
72  public static function schema(FieldStorageDefinitionInterface $field_definition) {
73    $columns = [];
74    foreach (static::$components as $key) {
75      $columns[$key] = [
76        'type' => 'varchar',
77        'length' => 255,
78        'not null' => FALSE,
79      ];
80    }
81    return [
82      'columns' => $columns,
83      'indexes' => [
84        'given' => ['given'],
85        'family' => ['family'],
86      ],
87    ];
88  }
89
90  /**
91   * {@inheritdoc}
92   */
93  public static function defaultFieldSettings() {
94    $settings = self::getDefaultNameFieldSettings();
95    $settings += self::getDefaultNameFormDisplaySettings();
96    $settings += self::getDefaultAdditionalPreferredSettings();
97    $settings['override_format'] = 'default';
98    return $settings + parent::defaultFieldSettings();
99  }
100
101  /**
102   * {@inheritdoc}
103   */
104  public static function propertyDefinitions(FieldStorageDefinitionInterface $field_definition) {
105    $properties = [];
106    $properties['title'] = DataDefinition::create('string')
107      ->setLabel(t('Title'));
108
109    $properties['given'] = DataDefinition::create('string')
110      ->setLabel(t('Given'));
111
112    $properties['middle'] = DataDefinition::create('string')
113      ->setLabel(t('Middle name(s)'));
114
115    $properties['family'] = DataDefinition::create('string')
116      ->setLabel(t('Family'));
117
118    $properties['generational'] = DataDefinition::create('string')
119      ->setLabel(t('Generational'));
120
121    $properties['credentials'] = DataDefinition::create('string')
122      ->setLabel(t('Credentials'));
123
124    return $properties;
125  }
126
127  /**
128   * {@inheritdoc}
129   */
130  public static function mainPropertyName() {
131    // There is no main property for this field item.
132    return NULL;
133  }
134
135  /**
136   * Whether any non-computed property has a value.
137   */
138  private function hasPopulatedProperty(): bool {
139    foreach ($this->properties as $property) {
140      $definition = $property->getDataDefinition();
141      $has_value = (
142        !$definition->isComputed() && $property->getValue() !== NULL
143      );
144      if ($has_value) {
145        return TRUE;
146      }
147    }
148
149    return FALSE;
150  }
151
152  /**
153   * Whether raw values contain meaningful content without properties.
154   */
155  private function hasMeaningfulRawValues(): bool {
156    if (!isset($this->values)) {
157      return FALSE;
158    }
159
160    foreach ($this->values as $name => $value) {
161      if (isset($this->properties[$name])) {
162        continue;
163      }
164      if (NameComponents::hasMeaningfulRawValue($name, $value)) {
165        return TRUE;
166      }
167    }
168
169    return FALSE;
170  }
171
172  /**
173   * {@inheritdoc}
174   */
175  public function isEmpty() {
176    if ($this->hasPopulatedProperty()) {
177      return FALSE;
178    }
179
180    return !$this->hasMeaningfulRawValues();
181  }
182
183  /**
184   * Returns active components only.
185   *
186   * @return array
187   *   Array of filtered name component values.
188   */
189  public function filteredArray() {
190    $values = [];
191    $field = $this->getFieldDefinition();
192    $settings = $field->getSettings();
193    $active_components = array_filter($settings['components']);
194    foreach ($this->getProperties() as $name => $property) {
195      $is_active = (isset($active_components[$name]) && $active_components[$name]);
196      if ($is_active) {
197        $values[$name] = $property->getValue();
198      }
199    }
200    return $values;
201  }
202
203  /**
204   * Get a list of active components.
205   *
206   * @return array
207   *   Keyed array of active component labels.
208   */
209  public function activeComponents() {
210    $settings = $this->getFieldDefinition()->getSettings();
211    $metadata = \Drupal::getContainer()
212      ->get('name.component_metadata', ContainerInterface::NULL_ON_INVALID_REFERENCE);
213    $translations = $metadata ? $metadata->getTranslations() : [];
214    $components = [];
215    foreach ($translations as $key => $label) {
216      if (!empty($settings['components'][$key])) {
217        $components[$key] = empty($settings['labels'][$key]) ? $label : $settings['labels'][$key];
218      }
219    }
220
221    return $components;
222  }
223
224  /**
225   * {@inheritdoc}
226   */
227  public function fieldSettingsForm(array $form, FormStateInterface $form_state) {
228    $settings = $this->getSettings();
229    $element = $this->getDefaultNameFieldSettingsForm($settings);
230    $element += $this->getDefaultNameFormDisplaySettingsForm($settings);
231    foreach ($this->getNameAdditionalPreferredSettingsForm() as $key => $value) {
232      $element[$key] = $value;
233      $element[$key]['#table_group'] = 'none';
234      $element[$key]['#weight'] = 50;
235    }
236
237    $element['#pre_render'][] = [$this, 'fieldSettingsFormPreRender'];
238
239    // Add the overwrite user name option.
240    if ($this->getFieldDefinition()->getTargetEntityTypeId() == 'user') {
241
242      $preferred_field = \Drupal::config('name.settings')
243        ->get('user_preferred');
244
245      $element['name_user_preferred'] = [
246        '#type' => 'checkbox',
247        '#title' => $this->t("Use this field to override the user's login name?"),
248        '#description' => $this->t('You may need to clear the @cache_link before this change is seen everywhere.', [
249          '@cache_link' => Link::fromTextAndUrl('Performance cache', Url::fromRoute('system.performance_settings'))->toString(),
250        ]),
251        '#default_value' => (($preferred_field == $this->getFieldDefinition()->getName()) ? 1 : 0),
252        '#table_group' => 'above',
253        '#weight' => -100,
254      ];
255
256      // Store the machine name of the Name field.
257      $element['name_user_preferred_fieldname'] = [
258        '#type' => 'hidden',
259        '#default_value' => $this->getFieldDefinition()->getName(),
260        '#table_group' => 'above',
261        '#weight' => -99,
262      ];
263
264      $element['override_format'] = [
265        '#type' => 'select',
266        '#title' => $this->t('User name override format to use'),
267        '#default_value' => $this->getSetting('override_format'),
268        '#options' => (\Drupal::getContainer()
269          ->get('name.format_options', ContainerInterface::NULL_ON_INVALID_REFERENCE))?->getCustomFormatOptions() ?? [],
270        '#table_group' => 'above',
271        '#weight' => -98,
272      ];
273
274      $element['#element_validate'] = [[get_class($this), 'validateUserPreferred']];
275      return $element;
276    }
277
278    // We may extend this feature to Profile2 latter.
279    $element['override_format'] = [
280      '#type' => 'value',
281      '#value' => $this->getSetting('override_format'),
282      '#table_group' => 'none',
283    ];
284
285    return $element;
286  }
287
288  /**
289   * Manage whether the name field should override a user's login name.
290   *
291   * @param array $element
292   *   The element being validated.
293   * @param \Drupal\Core\Form\FormStateInterface $form_state
294   *   The form state.
295   * @param array $complete_form
296   *   The complete form.
297   */
298  public static function validateUserPreferred(&$element, FormStateInterface $form_state, &$complete_form) {
299    unset($form_state, $complete_form);
300
301    $value = NULL;
302    $config = \Drupal::configFactory()->getEditable('name.settings');
303
304    // Ensure the name field value should override a user's login name.
305    $should_override_login = (
306      (!empty($element['name_user_preferred']))
307      && ($element['name_user_preferred']['#value'] == 1)
308    );
309    if ($should_override_login) {
310      // Retrieve the name field's machine name.
311      $value = $element['name_user_preferred_fieldname']['#default_value'];
312    }
313
314    // Ensure that the login-name-override configuration has changed.
315    if ($config->get('user_preferred') != $value) {
316
317      // Update the configuration with the new value.
318      $config->set('user_preferred', $value)->save();
319
320      // Retrieve the ID of all existing users.
321      $query = \Drupal::entityQuery('user');
322      $uids = $query->accessCheck(FALSE)->execute();
323
324      foreach ($uids as $uid) {
325        // Invalidate the cache for each user so that
326        // the appropriate login name will be displayed.
327        Cache::invalidateTags(["user:{$uid}"]);
328        \Drupal::logger('name')->notice('Cache cleared for data tagged as %tag.', [
329          '%tag' => "user:{$uid}",
330        ]);
331      }
332    }
333  }
334
335  /**
336   * {@inheritdoc}
337   */
338  public static function generateSampleValue(FieldDefinitionInterface $field_definition) {
339    // Single reused generation of 100 random names.
340    $names = &drupal_static(__FUNCTION__, []);
341    if (empty($names)) {
342      $names = \Drupal::service('name.generator')->generateSampleNames(100, $field_definition);
343    }
344    return $names[array_rand($names)];
345  }
346
347}