Code Coverage |
||||||||||||||||
Lines |
Branches |
Paths |
Functions and Methods |
Classes and Traits |
||||||||||||
| Total | |
0.00% |
0 / 94 |
|
0.00% |
0 / 35 |
|
0.00% |
0 / 28 |
|
0.00% |
0 / 8 |
CRAP | |
0.00% |
0 / 1 |
| ExtraFieldSource | |
0.00% |
0 / 85 |
|
0.00% |
0 / 35 |
|
0.00% |
0 / 28 |
|
0.00% |
0 / 8 |
420 | |
0.00% |
0 / 1 |
| create | |
0.00% |
0 / 4 |
|
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
| defaultSettings | |
0.00% |
0 / 3 |
|
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
| settingsSummary | |
0.00% |
0 / 9 |
|
0.00% |
0 / 9 |
|
0.00% |
0 / 7 |
|
0.00% |
0 / 1 |
30 | |||
| getPropValue | |
0.00% |
0 / 14 |
|
0.00% |
0 / 9 |
|
0.00% |
0 / 5 |
|
0.00% |
0 / 1 |
20 | |||
| settingsForm | |
0.00% |
0 / 9 |
|
0.00% |
0 / 4 |
|
0.00% |
0 / 3 |
|
0.00% |
0 / 1 |
6 | |||
| renderPlaceholder | |
0.00% |
0 / 6 |
|
0.00% |
0 / 3 |
|
0.00% |
0 / 2 |
|
0.00% |
0 / 1 |
6 | |||
| getDefinitions | |
0.00% |
0 / 5 |
|
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
| buildExtraField | |
0.00% |
0 / 35 |
|
0.00% |
0 / 7 |
|
0.00% |
0 / 8 |
|
0.00% |
0 / 1 |
20 | |||
| 1 | <?php |
| 2 | |
| 3 | declare(strict_types=1); |
| 4 | |
| 5 | namespace Drupal\display_builder_entity_view\Plugin\UiPatterns\Source; |
| 6 | |
| 7 | use Drupal\Core\Entity\Entity\EntityViewDisplay; |
| 8 | use Drupal\Core\Entity\EntityFieldManagerInterface; |
| 9 | use Drupal\Core\Entity\EntityTypeManagerInterface; |
| 10 | use Drupal\Core\Entity\FieldableEntityInterface; |
| 11 | use Drupal\Core\Form\FormStateInterface; |
| 12 | use Drupal\Core\Plugin\Context\ContextDefinition; |
| 13 | use Drupal\Core\StringTranslation\TranslatableMarkup; |
| 14 | use Drupal\display_builder\RenderableBuilderTrait; |
| 15 | use Drupal\ui_patterns\Attribute\Source; |
| 16 | use Drupal\ui_patterns\SourcePluginBase; |
| 17 | use 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 | )] |
| 32 | class 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 | } |
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.
| 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 | } |
| 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 | } |
| 62 | 'field' => NULL, |
| 63 | ]; |
| 64 | } |
| 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 | } |
| 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 | } |
| 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 | } |
| 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 | } |
| 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 | } |
| 222 | function ($e, $vm) { |
| 223 | // @phpstan-ignore-next-line |
| 224 | return $this->getBuildDefaults($e, $vm); |
| 225 | }, |