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}

Branches

Below are the source code lines that represent each code branch as identified by Xdebug. Please note a branch is not necessarily coterminous with a line, a line may contain multiple branches and therefore show up more than once. Please also be aware that some branches may be implicit rather than explicit, e.g. an if statement always has an else as part of its logical flow even if you didn't write one.

ExtraFieldSource->buildExtraField
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);
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]);
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'])) {
264        '#type' => 'html_tag',
265        '#tag' => 'span',
266        'lazy' => $field_build,
270    return $field_build;
271  }
ExtraFieldSource->create
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  }
ExtraFieldSource->defaultSettings
62      'field' => NULL,
63    ];
64  }
ExtraFieldSource->getDefinitions
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  }
ExtraFieldSource->getPropValue
93    $field = $this->getSetting('field');
94
95    if (!$field) {
96      return [];
100    $entity = $this->getContextValue('entity');
101    $view_mode = $this->getContextValue('view_mode') ?? 'default';
102
103    if (!$entity->id()) {
104      return $this->renderPlaceholder($field);
111    static $rendering = [];
112    $render_key = $entity->getEntityTypeId() . ':' . $entity->id() . ':' . $field . ':' . $view_mode;
113
114    if (isset($rendering[$render_key])) {
115      return [];
117    $rendering[$render_key] = TRUE;
118
119    try {
120      return $this->buildExtraField($entity, $field, $view_mode);
120      return $this->buildExtraField($entity, $field, $view_mode);
123      unset($rendering[$render_key]);
124    }
125  }
ExtraFieldSource->renderPlaceholder
154  protected function renderPlaceholder(string $field): array {
155    $definition = $this->getDefinitions()[$field] ?? NULL;
156
157    if (!$definition) {
158      return [];
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  }
ExtraFieldSource->settingsForm
130  public function settingsForm(array $form, FormStateInterface $form_state): array {
131    $options = ['' => $this->t('- Select -')];
132
133    foreach ($this->getDefinitions() as $field_name => $definition) {
133    foreach ($this->getDefinitions() as $field_name => $definition) {
133    foreach ($this->getDefinitions() as $field_name => $definition) {
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  }
ExtraFieldSource->settingsSummary
70    $field = $this->getSetting('field');
71
72    if (!$field) {
73      return [$this->t('No field selected')];
76    $definitions = $this->getDefinitions();
77
78    if (!$definitions || !isset($definitions[$field])) {
78    if (!$definitions || !isset($definitions[$field])) {
78    if (!$definitions || !isset($definitions[$field])) {
79      return [];
82    if (empty($definitions[$field]['label'])) {
83      return [$this->t('No field selected')];
86    return [\strip_tags((string) $definitions[$field]['label'])];
87  }
{closure:/var/www/html/web/modules/custom/display_builder/modules/display_builder_entity_view/src/Plugin/UiPatterns/Source/ExtraFieldSource.php:222-225}
222      function ($e, $vm) {
223        // @phpstan-ignore-next-line
224        return $this->getBuildDefaults($e, $vm);
225      },