Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
100.00% covered (success)
100.00%
56 / 56
100.00% covered (success)
100.00%
7 / 7
CRAP
100.00% covered (success)
100.00%
1 / 1
AdditionalComponentService
100.00% covered (success)
100.00%
56 / 56
100.00% covered (success)
100.00%
7 / 7
27
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
 getAdditionalComponent
100.00% covered (success)
100.00%
11 / 11
100.00% covered (success)
100.00%
1 / 1
5
 resolveSelf
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
2
 resolveSelfProperty
100.00% covered (success)
100.00%
5 / 5
100.00% covered (success)
100.00%
1 / 1
3
 resolveField
100.00% covered (success)
100.00%
17 / 17
100.00% covered (success)
100.00%
1 / 1
7
 collectEntityReferenceLabels
100.00% covered (success)
100.00%
10 / 10
100.00% covered (success)
100.00%
1 / 1
5
 collectRenderedValues
100.00% covered (success)
100.00%
11 / 11
100.00% covered (success)
100.00%
1 / 1
4
1<?php
2
3declare(strict_types=1);
4
5namespace Drupal\name\Service;
6
7use Drupal\Component\Utility\Html;
8use Drupal\Core\Entity\EntityInterface;
9use Drupal\Core\Entity\EntityTypeManagerInterface;
10use Drupal\Core\Entity\FieldableEntityInterface;
11use Drupal\Core\Field\FieldItemListInterface;
12use Drupal\Core\Render\RendererInterface;
13
14/**
15 * Resolves preferred/alternative field reference values for name formatting.
16 *
17 * @internal
18 */
19class AdditionalComponentService implements AdditionalComponentInterface {
20
21  public function __construct(
22    private readonly EntityTypeManagerInterface $entityTypeManager,
23    private readonly RendererInterface $renderer,
24  ) {}
25
26  /**
27   * Gets the rendered or label value for an additional component reference.
28   */
29  public function getAdditionalComponent(FieldItemListInterface $items, mixed $key_value, mixed $sep_value): string {
30    if (!$key_value) {
31      return '';
32    }
33
34    $parent = $items->getEntity();
35    $key    = (string) $key_value;
36
37    if ($key === '_self') {
38      return $this->resolveSelf($parent);
39    }
40
41    if (!$parent instanceof FieldableEntityInterface) {
42      return '';
43    }
44
45    if (str_starts_with($key, '_self_property')) {
46      return $this->resolveSelfProperty($parent, $key);
47    }
48
49    return $this->resolveField($parent, $key, $sep_value);
50  }
51
52  /**
53   * Resolves the parent entity label.
54   */
55  private function resolveSelf(EntityInterface $parent): string {
56    return (string) ($parent->label() ?: '');
57  }
58
59  /**
60   * Resolves a scalar value from a parent entity property.
61   */
62  private function resolveSelfProperty(FieldableEntityInterface $parent, string $key): string {
63    $property = str_replace('_self_property_', '', $key);
64
65    try {
66      $item = $parent->get($property);
67    }
68    catch (\InvalidArgumentException $e) {
69      return '';
70    }
71
72    return empty($item->value) ? '' : (string) $item->value;
73  }
74
75  /**
76   * Resolves labels or rendered values from a parent field.
77   */
78  private function resolveField(FieldableEntityInterface $parent, string $key, mixed $sep_value): string {
79    if (!$parent->hasField($key)) {
80      return '';
81    }
82
83    $target_items = $parent->get($key);
84    $field_is_inaccessible = (
85      $target_items->isEmpty() || !$target_items->access('view')
86    );
87    if ($field_is_inaccessible) {
88      return '';
89    }
90
91    $field  = $target_items->getFieldDefinition();
92    $values = match ($field->getType()) {
93      'entity_reference' => $this->collectEntityReferenceLabels($target_items),
94      default => $this->collectRenderedValues($parent, $target_items),
95    };
96
97    if (!$values) {
98      return '';
99    }
100
101    $separator = Html::decodeEntities(trim(strip_tags((string) $sep_value)));
102    return implode($separator, $values);
103  }
104
105  /**
106   * Collects labels from viewable referenced entities.
107   *
108   * @return string[]
109   *   The collected entity labels.
110   */
111  private function collectEntityReferenceLabels(FieldItemListInterface $target_items): array {
112    $values = [];
113
114    foreach ($target_items as $item) {
115      $entity_is_inaccessible = (
116        empty($item->entity) || !$item->entity->access('view')
117      );
118      if ($entity_is_inaccessible) {
119        continue;
120      }
121
122      $label = $item->entity->label();
123      if ($label) {
124        $values[] = $label;
125      }
126    }
127
128    return $values;
129  }
130
131  /**
132   * Collects rendered, unescaped values from field items.
133   *
134   * @return string[]
135   *   The collected rendered values.
136   */
137  private function collectRenderedValues(FieldableEntityInterface $parent, FieldItemListInterface $target_items): array {
138    $values       = [];
139    $view_builder = $this->entityTypeManager->getViewBuilder($parent->getEntityTypeId());
140
141    foreach ($target_items as $item) {
142      try {
143        $renderable = $view_builder->viewFieldItem($item, ['label' => 'hidden']);
144        /** @var \Drupal\Component\Render\MarkupInterface|string $value */
145        $value = (string) $this->renderer->render($renderable);
146      }
147      catch (\Exception $e) {
148        continue;
149      }
150
151      // Remove any markup, but decode entities as the parser requires raw
152      // unescaped strings.
153      $value = trim(strip_tags($value));
154      if ($value) {
155        $values[] = Html::decodeEntities($value);
156      }
157    }
158
159    return $values;
160  }
161
162}