Code Coverage
 
Lines
Branches
Paths
Functions and Methods
Classes and Traits
Total
0.00% covered (danger)
0.00%
0 / 94
0.00% covered (danger)
0.00%
0 / 35
0.00% covered (danger)
0.00%
0 / 28
0.00% covered (danger)
0.00%
0 / 8
CRAP
0.00% covered (danger)
0.00%
0 / 1
ExtraFieldSource
0.00% covered (danger)
0.00%
0 / 85
0.00% covered (danger)
0.00%
0 / 35
0.00% covered (danger)
0.00%
0 / 28
0.00% covered (danger)
0.00%
0 / 8
420
0.00% covered (danger)
0.00%
0 / 1
 create
0.00% covered (danger)
0.00%
0 / 4
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 defaultSettings
0.00% covered (danger)
0.00%
0 / 3
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 settingsSummary
0.00% covered (danger)
0.00%
0 / 9
0.00% covered (danger)
0.00%
0 / 9
0.00% covered (danger)
0.00%
0 / 7
0.00% covered (danger)
0.00%
0 / 1
30
 getPropValue
0.00% covered (danger)
0.00%
0 / 14
0.00% covered (danger)
0.00%
0 / 9
0.00% covered (danger)
0.00%
0 / 5
0.00% covered (danger)
0.00%
0 / 1
20
 settingsForm
0.00% covered (danger)
0.00%
0 / 9
0.00% covered (danger)
0.00%
0 / 4
0.00% covered (danger)
0.00%
0 / 3
0.00% covered (danger)
0.00%
0 / 1
6
 renderPlaceholder
0.00% covered (danger)
0.00%
0 / 6
0.00% covered (danger)
0.00%
0 / 3
0.00% covered (danger)
0.00%
0 / 2
0.00% covered (danger)
0.00%
0 / 1
6
 getDefinitions
0.00% covered (danger)
0.00%
0 / 5
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 buildExtraField
0.00% covered (danger)
0.00%
0 / 35
0.00% covered (danger)
0.00%
0 / 7
0.00% covered (danger)
0.00%
0 / 8
0.00% covered (danger)
0.00%
0 / 1
20
1<?php
2
3declare(strict_types=1);
4
5namespace Drupal\display_builder_entity_view\Plugin\UiPatterns\Source;
6
7use Drupal\Core\Entity\Entity\EntityViewDisplay;
8use Drupal\Core\Entity\EntityFieldManagerInterface;
9use Drupal\Core\Entity\EntityTypeManagerInterface;
10use Drupal\Core\Entity\FieldableEntityInterface;
11use Drupal\Core\Form\FormStateInterface;
12use Drupal\Core\Plugin\Context\ContextDefinition;
13use Drupal\Core\StringTranslation\TranslatableMarkup;
14use Drupal\display_builder\RenderableBuilderTrait;
15use Drupal\ui_patterns\Attribute\Source;
16use Drupal\ui_patterns\SourcePluginBase;
17use Symfony\Component\DependencyInjection\ContainerInterface;
18
19/**
20 * Plugin implementation of the source.
21 */
22#[Source(
23  id: 'extra_field',
24  label: new TranslatableMarkup('Extra field'),
25  description: new TranslatableMarkup('An entity extra field.'),
26  prop_types: ['slot'],
27  context_definitions: [
28    'entity' => new ContextDefinition('entity', label: new TranslatableMarkup('Entity'), required: TRUE),
29    'view_mode' => new ContextDefinition('string', label: new TranslatableMarkup('View mode'), required: FALSE),
30  ]
31)]
32class ExtraFieldSource extends SourcePluginBase {
33
34  use RenderableBuilderTrait;
35
36  /**
37   * The entity field manager.
38   */
39  protected EntityFieldManagerInterface $entityFieldManager;
40
41  /**
42   * The entity type manager.
43   */
44  protected EntityTypeManagerInterface $entityTypeManager;
45
46  /**
47   * {@inheritdoc}
48   */
49  public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition): static {
50    $instance = parent::create($container, $configuration, $plugin_id, $plugin_definition);
51    $instance->entityFieldManager = $container->get('entity_field.manager');
52    $instance->entityTypeManager = $container->get('entity_type.manager');
53
54    return $instance;
55  }
56
57  /**
58   * {@inheritdoc}
59   */
60  public function defaultSettings(): array {
61    return [
62      'field' => NULL,
63    ];
64  }
65
66  /**
67   * {@inheritdoc}
68   */
69  public function settingsSummary(): array {
70    $field = $this->getSetting('field');
71
72    if (!$field) {
73      return [$this->t('No field selected')];
74    }
75
76    $definitions = $this->getDefinitions();
77
78    if (!$definitions || !isset($definitions[$field])) {
79      return [];
80    }
81
82    if (empty($definitions[$field]['label'])) {
83      return [$this->t('No field selected')];
84    }
85
86    return [\strip_tags((string) $definitions[$field]['label'])];
87  }
88
89  /**
90   * {@inheritdoc}
91   */
92  public function getPropValue(): mixed {
93    $field = $this->getSetting('field');
94
95    if (!$field) {
96      return [];
97    }
98
99    /** @var \Drupal\Core\Entity\FieldableEntityInterface $entity */
100    $entity = $this->getContextValue('entity');
101    $view_mode = $this->getContextValue('view_mode') ?? 'default';
102
103    if (!$entity->id()) {
104      return $this->renderPlaceholder($field);
105    }
106
107    // Prevent infinite recursion: buildComponents() calls
108    // display->buildMultiple() which triggers
109    // EntityViewDisplayTrait::buildMultiple() → buildSources() →
110    // ExtraFieldSource::getPropValue() when Display builder is enabled.
111    static $rendering = [];
112    $render_key = $entity->getEntityTypeId() . ':' . $entity->id() . ':' . $field . ':' . $view_mode;
113
114    if (isset($rendering[$render_key])) {
115      return [];
116    }
117    $rendering[$render_key] = TRUE;
118
119    try {
120      return $this->buildExtraField($entity, $field, $view_mode);
121    }
122    finally {
123      unset($rendering[$render_key]);
124    }
125  }
126
127  /**
128   * {@inheritdoc}
129   */
130  public function settingsForm(array $form, FormStateInterface $form_state): array {
131    $options = ['' => $this->t('- Select -')];
132
133    foreach ($this->getDefinitions() as $field_name => $definition) {
134      $options[$field_name] = $definition['label'];
135    }
136    $form['field'] = [
137      '#type' => 'select',
138      '#options' => $options,
139      '#default_value' => $this->getSetting('field'),
140    ];
141
142    return $form;
143  }
144
145  /**
146   * Render placeholder when a proper entity is not loaded.
147   *
148   * @param string $field
149   *   Extra field ID.
150   *
151   * @return array
152   *   A renderable array.
153   */
154  protected function renderPlaceholder(string $field): array {
155    $definition = $this->getDefinitions()[$field] ?? NULL;
156
157    if (!$definition) {
158      return [];
159    }
160
161    $label = $definition['label'] ?? $this->t('No field selected');
162    $build = $this->buildPlaceholderButton($this->t('Extra field: @field', ['@field' => \strip_tags((string) $label)]));
163
164    return $build;
165  }
166
167  /**
168   * Get extra field definitions.
169   *
170   * Extra fields are not plugins but old-fashioned hooks.
171   *
172   * @throws \Drupal\Component\Plugin\Exception\ContextException
173   *
174   * @return array
175   *   A list of extra field definition.
176   */
177  protected function getDefinitions(): array {
178    /** @var \Drupal\Core\Entity\FieldableEntityInterface $entity */
179    $entity = $this->getContextValue('entity');
180    $entity_type_id = $entity->getEntityTypeId();
181    $bundle = $entity->bundle();
182
183    $extra_fields = $this->entityFieldManager->getExtraFields($entity_type_id, $bundle);
184
185    return $extra_fields['display'] ?? [];
186  }
187
188  /**
189   * Builds the extra field render array.
190   *
191   * @param \Drupal\Core\Entity\FieldableEntityInterface $entity
192   *   The entity.
193   * @param string $field
194   *   The extra field ID.
195   * @param string $view_mode
196   *   The view mode.
197   *
198   * @return array
199   *   The render array.
200   */
201  private function buildExtraField(FieldableEntityInterface $entity, string $field, string $view_mode): array {
202    $entity_type_id = $entity->getEntityTypeId();
203    $display = EntityViewDisplay::collectRenderDisplay($entity, $view_mode);
204
205    // Register the extra field as a component on the display. Implementations
206    // of hook_entity_view() and hook_ENTITY_TYPE_view() often guard rendering
207    // with $display->getComponent($field_name), which would silently skip
208    // the field if it is not enabled in the underlying entity view display.
209    if (!$display->getComponent($field)) {
210      $display->setComponent($field);
211    }
212
213    $view_builder = $this->entityTypeManager->getViewBuilder($entity_type_id);
214    $entity_key = (string) $entity->id();
215
216    // Build component additions from the concrete view builder implementation.
217    // For nodes this includes NodeViewBuilder::buildComponents(), which adds
218    // extras like 'links' and 'langcode' before view hooks run.
219    // Populate protected build defaults (e.g. CommentViewBuilder sets
220    // #comment_threaded) so buildComponents() receives a complete build array.
221    $get_defaults = \Closure::bind(
222      function ($e, $vm) {
223        // @phpstan-ignore-next-line
224        return $this->getBuildDefaults($e, $vm);
225      },
226      $view_builder,
227      \get_class($view_builder)
228    );
229    $defaults = $get_defaults($entity, $view_mode);
230    // Strip cache metadata and the entity key — handled separately below.
231    unset($defaults['#cache'], $defaults['#' . $entity_type_id]);
232    $build_list = [
233      $entity_key => $defaults + [
234        '#' . $entity_type_id => $entity,
235        '#view_mode' => $view_mode,
236      ],
237    ];
238    $view_builder->buildComponents($build_list, [$entity_key => $entity], [$entity->bundle() => $display], $view_mode);
239
240    /** @var array<string, mixed> $build */
241    $build = [];
242
243    if (isset($build_list[$entity_key])) {
244      $build += $build_list[$entity_key];
245    }
246
247    // Invoke both the entity-type-specific and the generic hook so all
248    // extra-field implementations are reached regardless of which hook they
249    // use. Core EntityViewBuilder does the same.
250    // @see \Drupal\Core\Entity\EntityViewBuilder::view()
251    $this->moduleHandler->invokeAll($entity_type_id . '_view', [&$build, $entity, $display, $view_mode]);
252    $this->moduleHandler->invokeAll('entity_view', [&$build, $entity, $display, $view_mode]);
253
254    $field_build = $build[$field] ?? [];
255
256    // Twig filters like |add_class and |set_attribute unconditionally add
257    // #attributes to whatever render array they receive.
258    // A #lazy_builder element cannot have sibling properties — the renderer
259    // asserts this at Renderer.php:396.
260    // Wrapping in a span means the filter targets the span element instead,
261    // so the class is rendered in the DOM and the #lazy_builder stays clean.
262    if (isset($field_build['#lazy_builder'])) {
263      return [
264        '#type' => 'html_tag',
265        '#tag' => 'span',
266        'lazy' => $field_build,
267      ];
268    }
269
270    return $field_build;
271  }
272
273}