Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
100.00% covered (success)
100.00%
108 / 108
100.00% covered (success)
100.00%
17 / 17
CRAP
100.00% covered (success)
100.00%
1 / 1
NameFormatterService
100.00% covered (success)
100.00%
108 / 108
100.00% covered (success)
100.00%
17 / 17
36
100.00% covered (success)
100.00%
1 / 1
 __construct
100.00% covered (success)
100.00%
10 / 10
100.00% covered (success)
100.00%
1 / 1
1
 setSetting
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 getSetting
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 format
100.00% covered (success)
100.00%
9 / 9
100.00% covered (success)
100.00%
1 / 1
3
 formatList
100.00% covered (success)
100.00%
13 / 13
100.00% covered (success)
100.00%
1 / 1
4
 formatNameItems
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
1 / 1
2
 shouldUseEtAl
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 limitItemsForEtAl
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
1 / 1
3
 formatEtAlList
100.00% covered (success)
100.00%
15 / 15
100.00% covered (success)
100.00%
1 / 1
3
 formatConjunctionList
100.00% covered (success)
100.00%
13 / 13
100.00% covered (success)
100.00%
1 / 1
4
 delimiterPrecedesLast
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
3
 getNameFormatString
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
1 / 1
2
 getListSettings
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
1 / 1
2
 getLastDelimitorTypes
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 getLastDelimiterTypes
100.00% covered (success)
100.00%
11 / 11
100.00% covered (success)
100.00%
1 / 1
2
 getLastDelimitorBehaviors
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 getLastDelimiterBehaviors
100.00% covered (success)
100.00%
11 / 11
100.00% covered (success)
100.00%
1 / 1
2
1<?php
2
3declare(strict_types=1);
4
5namespace Drupal\name\Service;
6
7use Drupal\Component\Render\FormattableMarkup;
8use Drupal\Component\Render\MarkupInterface;
9use Drupal\Component\Utility\Html;
10use Drupal\Core\Config\ConfigFactoryInterface;
11use Drupal\Core\Entity\EntityTypeManagerInterface;
12use Drupal\Core\Language\LanguageManagerInterface;
13use Drupal\Core\StringTranslation\StringTranslationTrait;
14use Drupal\Core\StringTranslation\TranslationInterface;
15use Drupal\name\Render\NameListFormattableMarkup;
16
17/**
18 * Primary name formatter for an array of name components.
19 *
20 * This service should be used for any name formatting requests and direct
21 * calls to the "name.format_parser" service should be avoided.
22 *
23 * Usage:
24 *   \Drupal::service('name.formatter')->format().
25 */
26class NameFormatterService implements NameFormatterInterface {
27
28  use StringTranslationTrait;
29
30  /**
31   * The name format parser.
32   */
33  protected NameFormatParserInterface $parser;
34
35  /**
36   * The name format storage.
37   *
38   * @var \Drupal\Core\Entity\EntityStorageInterface
39   */
40  protected $nameFormatStorage;
41
42  /**
43   * The name list format storage.
44   *
45   * @var \Drupal\Core\Entity\EntityStorageInterface
46   */
47  protected $listFormatStorage;
48
49  /**
50   * Language manager for retrieving the default language code if needed.
51   *
52   * @var \Drupal\Core\Language\LanguageManagerInterface
53   */
54  protected $languageManager;
55
56  /**
57   * The factory for configuration objects.
58   *
59   * @var \Drupal\Core\Config\ConfigFactoryInterface
60   */
61  protected $configFactory;
62
63  /**
64   * Settings for the formatter.
65   *
66   * Values include:
67   * - sep1: First defined separator.
68   * - sep2: Second defined separator.
69   * - sep3: Third defined separator.
70   * - markup: To markup the individual components.
71   *
72   * @var array
73   */
74  protected $settings = [
75    'sep1' => ' ',
76    'sep2' => ', ',
77    'sep3' => '',
78    'markup' => 'none',
79  ];
80
81  /**
82   * Constructs a name formatter object.
83   *
84   * @param \Drupal\Core\Entity\EntityTypeManagerInterface $entityTypeManager
85   *   The entity manager.
86   * @param \Drupal\name\Service\NameFormatParserInterface $parser
87   *   The name format parser.
88   * @param \Drupal\Core\Language\LanguageManagerInterface $language_manager
89   *   The language manager.
90   * @param \Drupal\Core\StringTranslation\TranslationInterface $translation
91   *   The string translation.
92   * @param \Drupal\Core\Config\ConfigFactoryInterface $config_factory
93   *   The factory for configuration objects.
94   */
95  public function __construct(EntityTypeManagerInterface $entityTypeManager, NameFormatParserInterface $parser, LanguageManagerInterface $language_manager, TranslationInterface $translation, ConfigFactoryInterface $config_factory) {
96    $this->nameFormatStorage = $entityTypeManager->getStorage('name_format');
97    $this->listFormatStorage = $entityTypeManager->getStorage('name_list_format');
98    $this->parser = $parser;
99    $this->languageManager = $language_manager;
100    $this->stringTranslation = $translation;
101    $this->configFactory = $config_factory;
102    $config = $this->configFactory->get('name.settings');
103    $this->settings['sep1'] = $config->get('sep1');
104    $this->settings['sep2'] = $config->get('sep2');
105    $this->settings['sep3'] = $config->get('sep3');
106  }
107
108  /**
109   * {@inheritdoc}
110   */
111  public function setSetting($key, $value) {
112    $this->settings[$key] = $value;
113    return $this;
114  }
115
116  /**
117   * {@inheritdoc}
118   */
119  public function getSetting($key) {
120    return $this->settings[$key] ?? NULL;
121  }
122
123  /**
124   * {@inheritdoc}
125   */
126  public function format(array $components, $type = 'default', $langcode = NULL) {
127    $format_string = $this->getNameFormatString($type);
128    $name = $this->parser->parse($components, $format_string, $this->settings);
129    if (!empty($components['url'])) {
130      $safe_name = $name instanceof MarkupInterface ? $name : Html::escape((string) $name);
131      $name = new FormattableMarkup('<a href=":link">@name</a>', [
132        ':link' => $components['url']->toString(),
133        '@name' => $safe_name,
134      ]);
135    }
136
137    return $name;
138  }
139
140  /**
141   * {@inheritdoc}
142   */
143  public function formatList(array $items, $type = 'default', $list_type = 'default', $langcode = NULL) {
144    $name_count = count($items);
145
146    // Avoid any computations if none or one names only.
147    if (!$name_count) {
148      return '';
149    }
150    if ($name_count == 1) {
151      $item = reset($items);
152      return $this->format($item, $type, $langcode);
153    }
154
155    $settings = $this->getListSettings($list_type);
156    $use_et_al = $this->shouldUseEtAl($settings, $name_count);
157    $items_to_format = $this->limitItemsForEtAl($items, $settings, $name_count);
158    $names = $this->formatNameItems($items_to_format, $type, $langcode);
159
160    if ($use_et_al) {
161      return $this->formatEtAlList($names, $settings);
162    }
163
164    return $this->formatConjunctionList($names, $settings, $name_count);
165  }
166
167  /**
168   * Formats a list of item component arrays using the selected name format.
169   *
170   * @param array<int, array<string, mixed>> $items
171   *   Name component arrays.
172   * @param string $type
173   *   Name format entity id.
174   * @param string|null $langcode
175   *   Language code or NULL for UI language.
176   *
177   * @return array<int, \Drupal\Component\Render\MarkupInterface|string>
178   *   A list of already formatted names.
179   */
180  protected function formatNameItems(array $items, string $type, ?string $langcode): array {
181    $names = [];
182    foreach ($items as $item) {
183      $names[] = $this->format($item, $type, $langcode);
184    }
185
186    return $names;
187  }
188
189  /**
190   * Determines whether list output should switch to et al style.
191   *
192   * @param array<string, mixed> $settings
193   *   Name list format settings.
194   * @param int $name_count
195   *   Total incoming number of names.
196   */
197  protected function shouldUseEtAl(array $settings, int $name_count): bool {
198    return $name_count > $settings['el_al_min'];
199  }
200
201  /**
202   * Removes names that don't need to be formatted for et al output.
203   *
204   * @param array<int, array<string, mixed>> $items
205   *   Name component arrays.
206   * @param array<string, mixed> $settings
207   *   Name list format settings.
208   * @param int $name_count
209   *   Total incoming number of names.
210   *
211   * @return array<int, array<string, mixed>>
212   *   The original list or a list reduced to the configured first names.
213   */
214  protected function limitItemsForEtAl(array $items, array $settings, int $name_count): array {
215    $should_limit = ($settings['el_al_min'] && $name_count > $settings['el_al_min']);
216    if ($should_limit) {
217      return array_slice($items, 0, $settings['el_al_first']);
218    }
219
220    return $items;
221  }
222
223  /**
224   * Formats a pre-formatted name list as an et al output.
225   *
226   * @param array<int, \Drupal\Component\Render\MarkupInterface|string> $names
227   *   A list of already formatted names.
228   * @param array<string, mixed> $settings
229   *   Name list format settings.
230   */
231  protected function formatEtAlList(array $names, array $settings): MarkupInterface {
232    $etal = $this->t('et al', [], ['context' => 'name']);
233    if ($this->settings['markup'] !== 'none') {
234      $etal = new FormattableMarkup('<em>@etal</em>', ['@etal' => $etal]);
235    }
236
237    if (count($names) == 1) {
238      return $this->t('@name@delimiter @etal', [
239        '@name' => reset($names),
240        '@delimiter' => trim($settings['delimiter']),
241        '@etal' => $etal,
242      ]);
243    }
244
245    $names = new NameListFormattableMarkup($names, $settings['delimiter']);
246    return $this->t('@names@delimiter @etal', [
247      '@names' => $names,
248      '@delimiter' => trim($settings['delimiter']),
249      '@etal' => $etal,
250    ]);
251  }
252
253  /**
254   * Formats a pre-formatted list with conjunction rules.
255   *
256   * @param array<int, \Drupal\Component\Render\MarkupInterface|string> $names
257   *   A list of already formatted names.
258   * @param array<string, mixed> $settings
259   *   Name list format settings.
260   * @param int $name_count
261   *   Total incoming number of names.
262   */
263  protected function formatConjunctionList(array $names, array $settings, int $name_count): MarkupInterface {
264    if ($settings['and'] == 'inherit') {
265      return new NameListFormattableMarkup($names, $settings['delimiter']);
266    }
267
268    $t_args = [
269      '@lastname' => array_pop($names),
270      '@names' => new NameListFormattableMarkup($names, $settings['delimiter']),
271      '@delimiter' => trim($settings['delimiter']),
272    ];
273    $t_args['@and'] = $settings['and'] == 'text'
274      ? $this->t('and', [], ['context' => 'name'])
275      : $this->t('&', [], ['context' => 'name']);
276
277    // Strange rule from citationstyles.org.
278    // @see http://citationstyles.org/downloads/specification.html
279    if ($this->delimiterPrecedesLast($settings, $name_count)) {
280      return $this->t('@names@delimiter @and @lastname', $t_args);
281    }
282
283    return $this->t('@names @and @lastname', $t_args);
284  }
285
286  /**
287   * Determines whether to include the delimiter before the final conjunction.
288   *
289   * @param array<string, mixed> $settings
290   *   Name list format settings.
291   * @param int $name_count
292   *   Total incoming number of names.
293   */
294  protected function delimiterPrecedesLast(array $settings, int $name_count): bool {
295    return ($settings['delimiter_precedes_last'] == 'contextual' && $name_count > 2)
296      || $settings['delimiter_precedes_last'] == 'always';
297  }
298
299  /**
300   * Helper function to get the format pattern.
301   *
302   * @param string $format
303   *   The ID of the preferred format to use. This will fallback to the default
304   *   format if the format can not be loaded.
305   *
306   * @return string
307   *   The pattern to parse.
308   */
309  protected function getNameFormatString($format) {
310    $config = $this->nameFormatStorage->load($format);
311    if (!$config) {
312      $config = $this->nameFormatStorage->load('default');
313    }
314    return $config->get('pattern');
315  }
316
317  /**
318   * Helper function to load and get the format list settings.
319   *
320   * @param string $format
321   *   The ID of the preferred format to use. This will fallback to the default
322   *   format if the format can not be loaded.
323   *
324   * @return array
325   *   The settings to use to format the list.
326   */
327  protected function getListSettings($format) {
328    /** @var \Drupal\name\Entity\NameListFormat $list_format */
329    $list_format = $this->listFormatStorage->load($format);
330    if (!$list_format) {
331      $list_format = $this->listFormatStorage->load('default');
332    }
333    return $list_format->listSettings();
334  }
335
336  /**
337   * {@inheritdoc}
338   */
339  public function getLastDelimitorTypes($include_examples = TRUE) {
340    // cspell:ignore delimitor
341    @trigger_error('getLastDelimitorTypes() is deprecated in name:8.x-1.1 and is removed from name:2.0.0. use getLastDelimiterTypes(). See https://www.drupal.org/project/name/issues/3518599', E_USER_DEPRECATED);
342    return $this->getLastDelimiterTypes($include_examples);
343  }
344
345  /**
346   * {@inheritdoc}
347   */
348  public function getLastDelimiterTypes($include_examples = TRUE) {
349    if (!$include_examples) {
350      return [
351        'text' => $this->t('Textual'),
352        'symbol' => $this->t('Ampersand'),
353        'inherit' => $this->t('Inherit delimiter'),
354      ];
355    }
356
357    return [
358      'text' => $this->t('Textual (and)'),
359      'symbol' => $this->t('Ampersand (&amp;)'),
360      'inherit' => $this->t('Inherit delimiter'),
361    ];
362  }
363
364  /**
365   * {@inheritdoc}
366   */
367  public function getLastDelimitorBehaviors($include_examples = TRUE) {
368    // cspell:ignore delimitor
369    @trigger_error('getLastDelimitorBehaviors() is deprecated in name:8.x-1.1 and is removed from name:2.0.0. use getLastDelimiterBehaviors(). See https://www.drupal.org/project/name/issues/3518599', E_USER_DEPRECATED);
370    return $this->getLastDelimiterBehaviors($include_examples);
371  }
372
373  /**
374   * {@inheritdoc}
375   */
376  public function getLastDelimiterBehaviors($include_examples = TRUE) {
377    if (!$include_examples) {
378      return [
379        'never' => $this->t('Never'),
380        'always' => $this->t('Always'),
381        'contextual' => $this->t('Contextual'),
382      ];
383    }
384
385    return [
386      'never' => $this->t('Never (i.e. "J. Doe and T. Williams")'),
387      'always' => $this->t('Always (i.e. "J. Doe<strong>,</strong> and T. Williams")'),
388      'contextual' => $this->t('Contextual (i.e. "J. Doe and T. Williams" <em>or</em> "J. Doe, S. Smith<strong>,</strong> and T. Williams")'),
389    ];
390  }
391
392}