Code Coverage
 
Lines
Branches
Paths
Functions and Methods
Classes and Traits
Total
0.00% covered (danger)
0.00%
0 / 61
0.00% covered (danger)
0.00%
0 / 18
0.00% covered (danger)
0.00%
0 / 13
0.00% covered (danger)
0.00%
0 / 7
CRAP
0.00% covered (danger)
0.00%
0 / 1
ExtraFieldSource
0.00% covered (danger)
0.00%
0 / 51
0.00% covered (danger)
0.00%
0 / 18
0.00% covered (danger)
0.00%
0 / 13
0.00% covered (danger)
0.00%
0 / 7
156
0.00% covered (danger)
0.00%
0 / 1
 create
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
 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 / 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
 getPropValue
0.00% covered (danger)
0.00%
0 / 21
0.00% covered (danger)
0.00%
0 / 7
0.00% covered (danger)
0.00%
0 / 4
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 / 7
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
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\Form\FormStateInterface;
10use Drupal\Core\Plugin\Context\ContextDefinition;
11use Drupal\Core\StringTranslation\TranslatableMarkup;
12use Drupal\display_builder\RenderableBuilderTrait;
13use Drupal\ui_patterns\Attribute\Source;
14use Drupal\ui_patterns\SourcePluginBase;
15use Symfony\Component\DependencyInjection\ContainerInterface;
16
17/**
18 * Plugin implementation of the source.
19 */
20#[Source(
21  id: 'extra_field',
22  label: new TranslatableMarkup('Extra field'),
23  description: new TranslatableMarkup('An entity extra field.'),
24  prop_types: ['slot'],
25  context_definitions: [
26    'entity' => new ContextDefinition('entity', label: new TranslatableMarkup('Entity'), required: TRUE),
27    'view_mode' => new ContextDefinition('string', label: new TranslatableMarkup('View mode'), required: FALSE),
28  ],
29  metadata: ['group' => new TranslatableMarkup('Fields')],
30)]
31class ExtraFieldSource extends SourcePluginBase {
32
33  use RenderableBuilderTrait;
34
35  /**
36   * The entity field manager.
37   */
38  protected EntityFieldManagerInterface $entityFieldManager;
39
40  /**
41   * {@inheritdoc}
42   */
43  public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition): static {
44    $instance = parent::create($container, $configuration, $plugin_id, $plugin_definition);
45    $instance->entityFieldManager = $container->get('entity_field.manager');
46
47    return $instance;
48  }
49
50  /**
51   * {@inheritdoc}
52   */
53  public function defaultSettings(): array {
54    return [
55      'field' => NULL,
56    ];
57  }
58
59  /**
60   * {@inheritdoc}
61   */
62  public function settingsSummary(): array {
63    return [
64      $this->getDefinitions()[$this->getSetting('field')]['label'] ?? '',
65    ];
66  }
67
68  /**
69   * {@inheritdoc}
70   */
71  public function getPropValue(): mixed {
72    $field = $this->getSetting('field');
73
74    if (!$field) {
75      return [];
76    }
77
78    /** @var \Drupal\Core\Entity\FieldableEntityInterface $entity */
79    $entity = $this->getContextValue('entity');
80    $view_mode = $this->getContextValue('view_mode') ?? 'default';
81
82    if (!$entity->id()) {
83      return $this->renderPlaceholder($field);
84    }
85
86    $entity_type_id = $entity->getEntityTypeId();
87    $display = EntityViewDisplay::collectRenderDisplay($entity, $view_mode);
88
89    // Register the extra field as a component on the display. Implementations
90    // of hook_entity_view() and hook_ENTITY_TYPE_view() often guard rendering
91    // with $display->getComponent($field_name), which would silently skip
92    // the field if it is not enabled in the underlying entity view display.
93    if (!$display->getComponent($field)) {
94      $display->setComponent($field);
95    }
96
97    // Mirror core EntityViewBuilder::buildComponents(): invoke prepare_view
98    // before the view hooks so implementations can set up their internal state.
99    // @see \Drupal\Core\Entity\EntityViewBuilder::buildComponents()
100    $this->moduleHandler->invokeAll('entity_prepare_view', [
101      $entity_type_id,
102      [$entity->id() => $entity],
103      [$entity->bundle() => $display],
104      $view_mode,
105    ]);
106
107    /** @var array<string, mixed> $build */
108    $build = [];
109
110    // Invoke both the entity-type-specific and the generic hook so all
111    // extra-field implementations are reached regardless of which hook they
112    // use. Core EntityViewBuilder does the same.
113    // @see \Drupal\Core\Entity\EntityViewBuilder::view()
114    $this->moduleHandler->invokeAll($entity_type_id . '_view', [&$build, $entity, $display, $view_mode]);
115    $this->moduleHandler->invokeAll('entity_view', [&$build, $entity, $display, $view_mode]);
116
117    return $build[$field] ?? [];
118  }
119
120  /**
121   * {@inheritdoc}
122   */
123  public function settingsForm(array $form, FormStateInterface $form_state): array {
124    $options = [];
125
126    foreach ($this->getDefinitions() as $field_name => $definition) {
127      $options[$field_name] = $definition['label'];
128    }
129    $form['field'] = [
130      '#type' => 'select',
131      '#options' => $options,
132      '#default_value' => $this->getSetting('field'),
133    ];
134
135    return $form;
136  }
137
138  /**
139   * Render placeholder when a proper entity is not loaded.
140   *
141   * @param string $field
142   *   Extra field ID.
143   *
144   * @return array
145   *   A renderable array.
146   */
147  protected function renderPlaceholder(string $field): array {
148    $definition = $this->getDefinitions()[$field];
149
150    if (!$definition) {
151      return [];
152    }
153
154    $label = $definition['label'] ?? '';
155    $build = $this->buildPlaceholderButton($this->t('Extra field: @field', ['@field' => $label]));
156    $build['#attributes']['class'][] = 'db-background';
157
158    return $build;
159  }
160
161  /**
162   * Get extra field definitions.
163   *
164   * Extra fields are not plugins but old-fashioned hooks.
165   *
166   * @throws \Drupal\Component\Plugin\Exception\ContextException
167   *
168   * @return array
169   *   A list of extra field definition.
170   */
171  protected function getDefinitions(): array {
172    /** @var \Drupal\Core\Entity\FieldableEntityInterface $entity */
173    $entity = $this->getContextValue('entity');
174    $entity_type_id = $entity->getEntityTypeId();
175    $bundle = $entity->bundle();
176
177    $extra_fields = $this->entityFieldManager->getExtraFields($entity_type_id, $bundle);
178
179    return $extra_fields['display'] ?? [];
180  }
181
182}