Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
100.00% covered (success)
100.00%
82 / 82
100.00% covered (success)
100.00%
8 / 8
CRAP
100.00% covered (success)
100.00%
1 / 1
GeneratorService
100.00% covered (success)
100.00%
82 / 82
100.00% covered (success)
100.00%
8 / 8
30
100.00% covered (success)
100.00%
1 / 1
 __construct
100.00% covered (success)
100.00%
5 / 5
100.00% covered (success)
100.00%
1 / 1
1
 generateSampleNames
100.00% covered (success)
100.00%
9 / 9
100.00% covered (success)
100.00%
1 / 1
4
 initComponents
100.00% covered (success)
100.00%
20 / 20
100.00% covered (success)
100.00%
1 / 1
7
 buildName
100.00% covered (success)
100.00%
27 / 27
100.00% covered (success)
100.00%
1 / 1
9
 pickRandom
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 loadSampleValues
100.00% covered (success)
100.00%
7 / 7
100.00% covered (success)
100.00%
1 / 1
3
 loadConfiguration
100.00% covered (success)
100.00%
6 / 6
100.00% covered (success)
100.00%
1 / 1
3
 filterByFieldSettings
100.00% covered (success)
100.00%
7 / 7
100.00% covered (success)
100.00%
1 / 1
2
1<?php
2
3declare(strict_types=1);
4
5namespace Drupal\name\Service;
6
7use Drupal\Core\Config\ConfigFactoryInterface;
8use Drupal\Core\Field\FieldDefinitionInterface;
9use Drupal\Core\Language\LanguageManagerInterface;
10use Drupal\Core\StringTranslation\StringTranslationTrait;
11use Drupal\Core\StringTranslation\TranslationInterface;
12use Drupal\name\Utility\NameComponents;
13
14/**
15 * Handles name generation.
16 */
17class GeneratorService implements GeneratorInterface {
18
19  use StringTranslationTrait;
20
21  /**
22   * The name formatter.
23   */
24  protected NameFormatterInterface $formatter;
25
26  /**
27   * The name format parser.
28   */
29  protected NameFormatParserInterface $parser;
30
31  /**
32   * The factory for configuration objects.
33   *
34   * @var \Drupal\Core\Config\ConfigFactoryInterface
35   */
36  protected $configFactory;
37
38  /**
39   * Language manager for retrieving the default language code if needed.
40   *
41   * @var \Drupal\Core\Language\LanguageManagerInterface
42   */
43  protected $languageManager;
44
45  /**
46   * An array of gender sorted components for generating random names.
47   *
48   * @var array
49   */
50  protected $components = [];
51
52  /**
53   * Constructs a name formatter object.
54   *
55   * @param \Drupal\name\Service\NameFormatterInterface $formatter
56   *   The name formatter.
57   * @param \Drupal\name\Service\NameFormatParserInterface $parser
58   *   The name format parser.
59   * @param \Drupal\Core\Config\ConfigFactoryInterface $config_factory
60   *   The factory for configuration objects.
61   * @param \Drupal\Core\Language\LanguageManagerInterface $language_manager
62   *   The language manager.
63   * @param \Drupal\Core\StringTranslation\TranslationInterface $translation
64   *   The string translation.
65   */
66  public function __construct(NameFormatterInterface $formatter, NameFormatParserInterface $parser, ConfigFactoryInterface $config_factory, LanguageManagerInterface $language_manager, TranslationInterface $translation) {
67    $this->formatter = $formatter;
68    $this->parser = $parser;
69    $this->configFactory = $config_factory;
70    $this->languageManager = $language_manager;
71    $this->stringTranslation = $translation;
72  }
73
74  /**
75   * {@inheritdoc}
76   */
77  public function generateSampleNames($limit = 3, ?FieldDefinitionInterface $field_definition = NULL) {
78    $this->initComponents($field_definition);
79    $preferred = $this->loadConfiguration('name.generate.preferred', 'preferred', $field_definition);
80
81    $names = [];
82    for ($i = 0; $i < $limit; $i += 1) {
83      $gender = rand(0, 1) ? 'male' : 'female';
84      $names[] = $this->buildName($gender, $preferred ?? []);
85    }
86
87    if ($field_definition) {
88      $names = $this->filterByFieldSettings($field_definition, $names);
89    }
90
91    return $names;
92  }
93
94  /**
95   * Initializes configured component pools for random name generation.
96   *
97   * @param \Drupal\Core\Field\FieldDefinitionInterface|null $field_definition
98   *   The field definition to find field specific configuration.
99   */
100  private function initComponents(?FieldDefinitionInterface $field_definition = NULL): void {
101    if ($this->components) {
102      return;
103    }
104
105    $keys = NameComponents::coreKeys() + [
106      'preferred'   => 'preferred',
107      'alternative' => 'alternative',
108    ];
109    $this->components = [
110      'female' => array_fill_keys($keys, []),
111      'male'   => array_fill_keys($keys, []),
112    ];
113
114    // Parse genderless configuration.
115    $components = $this->loadConfiguration('name.generate.components', 'components', $field_definition);
116    foreach ($keys as $key) {
117      if (isset($components[$key])) {
118        $this->components['female'][$key] = $components[$key];
119        $this->components['male'][$key] = $components[$key];
120      }
121    }
122
123    // Parse gender configuration.
124    $components = $this->loadConfiguration('name.generate.components', 'gender', $field_definition);
125    foreach (['female', 'male'] as $gender) {
126      foreach ($keys as $key) {
127        if (isset($components[$gender][$key])) {
128          $this->components[$gender][$key] = array_merge($this->components[$gender][$key], $components[$gender][$key]);
129        }
130      }
131    }
132  }
133
134  /**
135   * Builds a random name array for a gender component pool.
136   *
137   * @param string $gender
138   *   The gender component pool to use.
139   * @param array<string, string> $preferred
140   *   Preferred names keyed by given name.
141   *
142   * @return array<string, mixed>
143   *   A random name component array.
144   */
145  private function buildName(string $gender, array $preferred): array {
146    $name = [
147      'title'        => '',
148      'given'        => $this->pickRandom($this->components[$gender]['given']),
149      'middle'       => '',
150      'family'       => $this->pickRandom($this->components[$gender]['family']),
151      'generational' => '',
152      'credentials'  => '',
153    ];
154
155    if (rand(0, 2)) {
156      $name['title'] = $this->pickRandom($this->components[$gender]['title']);
157    }
158    if (rand(0, 1)) {
159      $name['middle'] = $this->pickRandom($this->components[$gender]['middle']);
160    }
161    if (rand(0, 1)) {
162      $credentials = [];
163      $credential_count = count($this->components[$gender]['credentials']);
164      $cred_limit = min([rand(1, 3), $credential_count]);
165      for ($j = 0; $j <= $cred_limit; $j += 1) {
166        $credentials[] = $this->pickRandom($this->components[$gender]['credentials']);
167      }
168      $name['credentials'] = implode(', ', $credentials);
169    }
170    if (!rand(0, 2)) {
171      $name['generational'] = $this->pickRandom($this->components[$gender]['generational']);
172    }
173    // All defined names have a preferred alternative, randomize it slightly.
174    $may_use_preferred = (
175      rand(0, 1) && !empty($name['given']) && !empty($preferred[$name['given']])
176    );
177    if ($may_use_preferred) {
178      $name['preferred'] = $preferred[$name['given']];
179    }
180
181    return $name;
182  }
183
184  /**
185   * Picks a random value from a component pool.
186   *
187   * @param array<int, mixed> $pool
188   *   The component pool.
189   *
190   * @return mixed
191   *   A random component value.
192   */
193  private function pickRandom(array $pool): mixed {
194    return $pool[array_rand($pool)];
195  }
196
197  /**
198   * {@inheritdoc}
199   */
200  public function loadSampleValues($limit = 3, ?FieldDefinitionInterface $field_definition = NULL, $random = FALSE) {
201    $example_names = $this->loadConfiguration('name.generate.examples', 'examples', $field_definition) ?? [];
202
203    // Randomly shuffle and get the required count.
204    if ($random) {
205      shuffle($example_names);
206    }
207
208    $example_names = array_slice($example_names, 0, $limit);
209
210    // Filter to the enabled components if we have field context.
211    if ($field_definition) {
212      $example_names = $this->filterByFieldSettings($field_definition, $example_names);
213    }
214
215    return $example_names;
216  }
217
218  /**
219   * Helper function to load the settings.
220   *
221   * @param string $config
222   *   The configuration to load.
223   * @param string $key
224   *   The configuration key to retrieve.
225   * @param \Drupal\Core\Field\FieldDefinitionInterface|null $field_definition
226   *   The field definition to find field specific configuration.
227   *
228   * @return array
229   *   The array of settings.
230   *
231   * @throws \Drupal\Core\Config\ConfigException
232   *   An error if the global configuration is empty or missing.
233   */
234  protected function loadConfiguration($config, $key, ?FieldDefinitionInterface $field_definition = NULL) {
235    $components = [];
236    if ($field_definition) {
237      $components = $this->configFactory->get($config . '.' . $field_definition->getName())->get($key);
238    }
239    if (!$components) {
240      $components = $this->configFactory->get($config)->get($key);
241    }
242
243    return $components;
244  }
245
246  /**
247   * Helper function to filter the name components by the field definition.
248   *
249   * @param \Drupal\Core\Field\FieldDefinitionInterface $field_definition
250   *   The field definition if in context.
251   * @param array $example_names
252   *   Array of names to filter.
253   *
254   * @return array
255   *   A nested array of name components.
256   */
257  protected function filterByFieldSettings(FieldDefinitionInterface $field_definition, array $example_names = []) {
258    $settings = $field_definition->getSettings();
259    $components = array_keys(array_filter($settings['components']));
260    $components = array_combine($components, $components);
261    foreach ($example_names as $delta => $example_name) {
262      $example_names[$delta] = array_intersect_key($example_name, $components);
263      // This allows non-field defined properties like preferred through.
264      $example_names[$delta] += array_diff_key($example_name, NameComponents::coreKeys());
265    }
266    return $example_names;
267  }
268
269}