Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
100.00% covered (success)
100.00%
52 / 52
100.00% covered (success)
100.00%
7 / 7
CRAP
100.00% covered (success)
100.00%
1 / 1
NameOptionService
100.00% covered (success)
100.00%
52 / 52
100.00% covered (success)
100.00%
7 / 7
21
100.00% covered (success)
100.00%
1 / 1
 __construct
100.00% covered (success)
100.00%
8 / 8
100.00% covered (success)
100.00%
1 / 1
3
 getOptions
100.00% covered (success)
100.00%
6 / 6
100.00% covered (success)
100.00%
1 / 1
1
 loadComponentOptions
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
1 / 1
1
 expandVocabularyOptions
100.00% covered (success)
100.00%
5 / 5
100.00% covered (success)
100.00%
1 / 1
3
 appendTermsFromVocabulary
100.00% covered (success)
100.00%
10 / 10
100.00% covered (success)
100.00%
1 / 1
6
 sortComponentOptionsIfEnabled
100.00% covered (success)
100.00%
6 / 6
100.00% covered (success)
100.00%
1 / 1
3
 buildKeyedOptions
100.00% covered (success)
100.00%
13 / 13
100.00% covered (success)
100.00%
1 / 1
4
1<?php
2
3declare(strict_types=1);
4
5namespace Drupal\name\Service;
6
7use Drupal\Core\Entity\EntityTypeManagerInterface;
8use Drupal\Core\Extension\ModuleHandlerInterface;
9use Drupal\Core\Field\FieldDefinitionInterface;
10
11/**
12 * Options provider for name field components.
13 */
14class NameOptionService implements NameOptionInterface {
15
16  /**
17   * The entity type manager.
18   */
19  protected EntityTypeManagerInterface $entityTypeManager;
20
21  /**
22   * The module handler.
23   */
24  protected ModuleHandlerInterface $moduleHandler;
25
26  /**
27   * The term storage manager.
28   *
29   * @var \Drupal\taxonomy\TermStorageInterface|null
30   */
31  protected $termStorage;
32
33  /**
34   * The vocabulary storage manager.
35   *
36   * @var \Drupal\taxonomy\VocabularyStorageInterface|null
37   */
38  protected $vocabularyStorage;
39
40  /**
41   * Constructs a NameOptionService object.
42   */
43  public function __construct(EntityTypeManagerInterface $entity_type_manager, ModuleHandlerInterface $module_handler) {
44    $this->entityTypeManager = $entity_type_manager;
45    $this->moduleHandler = $module_handler;
46
47    $has_taxonomy_storage = (
48      $this->entityTypeManager && $this->moduleHandler->moduleExists('taxonomy')
49    );
50    if ($has_taxonomy_storage) {
51      $this->termStorage = $this->entityTypeManager->getStorage('taxonomy_term');
52      $this->vocabularyStorage = $this->entityTypeManager->getStorage('taxonomy_vocabulary');
53    }
54  }
55
56  /**
57   * {@inheritdoc}
58   */
59  public function getOptions(FieldDefinitionInterface $field, string $component): array {
60    $settings = $field->getSettings();
61    $options = $this->loadComponentOptions($settings, $component);
62    $options = $this->expandVocabularyOptions($options, $settings, $component);
63    $options = array_unique($options);
64    $options = $this->sortComponentOptionsIfEnabled($options, $settings, $component);
65
66    return $this->buildKeyedOptions($options);
67  }
68
69  /**
70   * Loads raw string options from field settings.
71   *
72   * Field settings may store TranslatableMarkup or other Stringable values.
73   *
74   * @param array<string, mixed> $settings
75   *   Field settings.
76   * @param string $component
77   *   Name component key.
78   *
79   * @return list<string>
80   *   Option strings.
81   */
82  protected function loadComponentOptions(array $settings, string $component): array {
83    return array_map(
84      static fn ($item): string => (string) $item,
85      (array) ($settings[$component . '_options'] ?? [])
86    );
87  }
88
89  /**
90   * Replaces vocabulary tokens with taxonomy term names.
91   *
92   * @param list<string> $options
93   *   Raw option strings.
94   * @param array<string, mixed> $settings
95   *   Field settings.
96   * @param string $component
97   *   Name component key.
98   *
99   * @return list<string>
100   *   Options with vocabulary tokens expanded.
101   */
102  protected function expandVocabularyOptions(array $options, array $settings, string $component): array {
103    foreach ($options as $index => $opt) {
104      if (!preg_match(NameOptionInterface::VOCABULARY_REGEX, trim($opt), $matches)) {
105        continue;
106      }
107      unset($options[$index]);
108      $this->appendTermsFromVocabulary($options, $matches[1], $settings, $component);
109    }
110    return $options;
111  }
112
113  /**
114   * Appends taxonomy term names for a vocabulary into the options list.
115   *
116   * @param list<string> $options
117   *   Options list to append to.
118   * @param string $vocabulary_id
119   *   Taxonomy vocabulary machine name.
120   * @param array<string, mixed> $settings
121   *   Field settings.
122   * @param string $component
123   *   Name component key.
124   */
125  protected function appendTermsFromVocabulary(array &$options, string $vocabulary_id, array $settings, string $component): void {
126    $storage_is_unavailable = (!$this->termStorage || !$this->vocabularyStorage);
127    if ($storage_is_unavailable) {
128      return;
129    }
130    $vocabulary = $this->vocabularyStorage->load($vocabulary_id);
131    if (!$vocabulary) {
132      return;
133    }
134    $max_length = $settings['max_length'][$component] ?? 255;
135    foreach ($this->termStorage->loadTree($vocabulary->id()) as $term) {
136      if (mb_strlen($term->name) <= $max_length) {
137        $options[] = $term->name;
138      }
139    }
140  }
141
142  /**
143   * Sorts options when the field enables sort for this component.
144   *
145   * @param list<string> $options
146   *   Option strings.
147   * @param array<string, mixed> $settings
148   *   Field settings.
149   * @param string $component
150   *   Name component key.
151   *
152   * @return list<string>
153   *   Sorted options.
154   */
155  protected function sortComponentOptionsIfEnabled(array $options, array $settings, string $component): array {
156    $should_sort = (
157      isset($settings['sort_options']) && !empty($settings['sort_options'][$component])
158    );
159    if ($should_sort) {
160      natcasesort($options);
161    }
162    return $options;
163  }
164
165  /**
166   * Builds keyed options and maps a dashed placeholder to _none.
167   *
168   * @param list<string> $options
169   *   Option strings.
170   *
171   * @return array<string, string>
172   *   Options keyed by value.
173   */
174  protected function buildKeyedOptions(array $options): array {
175    $default = FALSE;
176    foreach ($options as $index => $opt) {
177      if (strpos($opt, '--') !== 0) {
178        continue;
179      }
180      unset($options[$index]);
181      $default = trim(mb_substr($opt, 2));
182    }
183    $options = array_map(
184      static fn (string $value): string => trim($value),
185      $options
186    );
187    $options = array_combine($options, $options);
188    if ($default !== FALSE) {
189      $options = ['_none' => $default] + $options;
190    }
191    return $options;
192  }
193
194}