Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
100.00% covered (success)
100.00%
52 / 52
100.00% covered (success)
100.00%
4 / 4
CRAP
100.00% covered (success)
100.00%
1 / 1
UserHooks
100.00% covered (success)
100.00%
52 / 52
100.00% covered (success)
100.00%
4 / 4
21
100.00% covered (success)
100.00%
1 / 1
 __construct
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 userFormatNameAlter
100.00% covered (success)
100.00%
12 / 12
100.00% covered (success)
100.00%
1 / 1
8
 userLoad
100.00% covered (success)
100.00%
31 / 31
100.00% covered (success)
100.00%
1 / 1
9
 resolvePreferredField
100.00% covered (success)
100.00%
8 / 8
100.00% covered (success)
100.00%
1 / 1
3
1<?php
2
3declare(strict_types=1);
4
5namespace Drupal\name\Hook;
6
7use Drupal\Core\Config\ConfigFactoryInterface;
8use Drupal\Core\Hook\Attribute\Hook;
9use Drupal\Core\Session\AccountInterface;
10use Drupal\field\Entity\FieldConfig;
11use Drupal\name\Service\AdditionalComponentInterface;
12use Drupal\name\Service\NameFormatterInterface;
13use Drupal\name\Service\UserRealnamePreloadInterface;
14use Symfony\Component\DependencyInjection\Attribute\Autowire;
15
16/**
17 * Hook implementations that integrate the user entity with realname lookups.
18 *
19 * @internal
20 */
21final class UserHooks {
22
23  /**
24   * Static cache of the preferred name FieldConfig for the user entity.
25   *
26   * Resolved once per request because it is keyed on a config value.
27   */
28  private ?FieldConfig $preferredField = NULL;
29
30  /**
31   * Whether the preferred field has been resolved (even to NULL).
32   */
33  private bool $preferredResolved = FALSE;
34
35  public function __construct(
36    private readonly ConfigFactoryInterface $configFactory,
37    #[Autowire(lazy: true)]
38    private readonly NameFormatterInterface $nameFormatter,
39    #[Autowire(lazy: true)]
40    private readonly AdditionalComponentInterface $additionalComponent,
41    private readonly UserRealnamePreloadInterface $realnamePreload,
42  ) {}
43
44  /**
45   * Implements hook_user_format_name_alter().
46   */
47  // phpcs:ignore Drupal.Commenting.PostStatementComment.Found -- #[Hook] is a PHP attribute, not a trailing comment.
48  #[Hook('user_format_name_alter')] // @phpstan-ignore attribute.notFound
49  public function userFormatNameAlter(&$name, AccountInterface $account): void {
50    // Don't alter anonymous users or objects that do not have any user ID.
51    if ($account->isAnonymous()) {
52      return;
53    }
54
55    // Try and load the realname in case this is a partial user object or
56    // another object, such as a node or comment.
57    if (!isset($account->realname)) {
58      $this->realnamePreload->preload($account);
59    }
60
61    // Since $account may not be the real User entity object, check the name
62    // lookup cache for results too.
63    $realname_is_missing = (!isset($account->realname) || !mb_strlen($account->realname));
64    if ($realname_is_missing) {
65      $names = &drupal_static('name_user_realname_cache', []);
66      if (isset($names[$account->id()])) {
67        $account->realname = $names[$account->id()];
68      }
69    }
70
71    $has_realname = (isset($account->realname) && mb_strlen($account->realname));
72    if ($has_realname) {
73      $name = $account->realname;
74    }
75  }
76
77  /**
78   * Implements hook_user_load().
79   */
80  // phpcs:ignore Drupal.Commenting.PostStatementComment.Found -- #[Hook] is a PHP attribute, not a trailing comment.
81  #[Hook('user_load')] // @phpstan-ignore attribute.notFound
82  public function userLoad(array $users): void {
83    // In the event there are a lot of user_load() calls, cache the results.
84    $names = &drupal_static('name_user_realname_cache', []);
85
86    $field = $this->resolvePreferredField();
87    if (!$field) {
88      return;
89    }
90
91    foreach ($users as $account) {
92      $uid = $account->id();
93      if (isset($names[$uid])) {
94        $users[$uid]->realname = $names[$uid];
95        continue;
96      }
97      $field_is_missing = (
98        !$account->hasField($field->getName())
99        || $account->get($field->getName())->isEmpty()
100      );
101      if ($field_is_missing) {
102        continue;
103      }
104      $components = $account->get($field->getName())->get(0)->getValue();
105      foreach (['preferred', 'alternative'] as $key) {
106        $key_value = $field->getSetting($key . '_field_reference');
107        if (!$key_value) {
108          continue;
109        }
110        $sep_value = $field->getSetting($key . '_field_reference_separator');
111        $value = $this->additionalComponent->getAdditionalComponent(
112          $account->get($field->getName()),
113          $key_value,
114          $sep_value,
115        );
116        if ($value) {
117          $components[$key] = $value;
118        }
119      }
120      $names[$uid] = $this->nameFormatter->format(
121        $components,
122        $field->getSetting('override_format'),
123      );
124      $users[$uid]->realname = $names[$uid];
125    }
126  }
127
128  /**
129   * Resolves the configured preferred user name field once per request.
130   */
131  private function resolvePreferredField(): ?FieldConfig {
132    if ($this->preferredResolved) {
133      return $this->preferredField;
134    }
135    $field_name = $this->configFactory->get('name.settings')->get('user_preferred');
136    $this->preferredField = $field_name
137      ? FieldConfig::loadByName('user', 'user', $field_name)
138      : NULL;
139    $this->preferredResolved = TRUE;
140    return $this->preferredField;
141  }
142
143}