Code Coverage
 
Lines
Branches
Paths
Functions and Methods
Classes and Traits
Total
68.97% covered (warning)
68.97%
40 / 58
62.07% covered (warning)
62.07%
18 / 29
42.11% covered (danger)
42.11%
8 / 19
22.22% covered (danger)
22.22%
2 / 9
CRAP
0.00% covered (danger)
0.00%
0 / 1
DisplayExtender
68.97% covered (warning)
68.97%
40 / 58
62.07% covered (warning)
62.07%
18 / 29
42.11% covered (danger)
42.11%
8 / 19
22.22% covered (danger)
22.22%
2 / 9
89.05
0.00% covered (danger)
0.00%
0 / 1
 create
100.00% covered (success)
100.00%
5 / 5
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 buildOptionsForm
75.00% covered (warning)
75.00%
3 / 4
66.67% covered (warning)
66.67%
2 / 3
50.00% covered (danger)
50.00%
1 / 2
0.00% covered (danger)
0.00%
0 / 1
2.50
 submitOptionsForm
90.00% covered (success)
90.00%
9 / 10
80.00% covered (warning)
80.00%
4 / 5
66.67% covered (warning)
66.67%
2 / 3
0.00% covered (danger)
0.00%
0 / 1
3.33
 optionsSummary
88.89% covered (warning)
88.89%
8 / 9
66.67% covered (warning)
66.67%
2 / 3
50.00% covered (danger)
50.00%
1 / 2
0.00% covered (danger)
0.00%
0 / 1
2.50
 preExecute
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
 buildThemeRegistryEntry
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
 getInstance
85.71% covered (warning)
85.71%
6 / 7
80.00% covered (warning)
80.00%
4 / 5
33.33% covered (danger)
33.33%
1 / 3
0.00% covered (danger)
0.00%
0 / 1
5.67
 isApplicable
66.67% covered (warning)
66.67%
6 / 9
57.14% covered (warning)
57.14%
4 / 7
25.00% covered (danger)
25.00%
1 / 4
0.00% covered (danger)
0.00%
0 / 1
10.75
 displayBuildable
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
1<?php
2
3declare(strict_types=1);
4
5namespace Drupal\display_builder_views\Plugin\views\display_extender;
6
7use Drupal\Core\Extension\ModuleExtensionList;
8use Drupal\Core\Form\FormStateInterface;
9use Drupal\Core\StringTranslation\TranslatableMarkup;
10use Drupal\Core\Theme\Registry;
11use Drupal\display_builder\DisplayBuildableInterface;
12use Drupal\display_builder\InstanceInterface;
13use Drupal\views\Attribute\ViewsDisplayExtender;
14use Drupal\views\Plugin\views\display_extender\DisplayExtenderPluginBase;
15use Symfony\Component\DependencyInjection\ContainerInterface;
16
17/**
18 * Styles display extender plugin.
19 *
20 * @ingroup views_display_extender_plugins
21 */
22#[ViewsDisplayExtender(
23  id: 'display_builder',
24  title: new TranslatableMarkup('Display Builder'),
25  help: new TranslatableMarkup('Use display builder as output for this view.'),
26  no_ui: FALSE,
27)]
28final class DisplayExtender extends DisplayExtenderPluginBase {
29
30  /**
31   * The entity type interface.
32   *
33   * @var \Drupal\Core\Entity\EntityTypeManagerInterface
34   */
35  protected $entityTypeManager;
36
37  /**
38   * The theme registry.
39   */
40  protected Registry $themeRegistry;
41
42  /**
43   * The list of modules.
44   */
45  protected ModuleExtensionList $modules;
46
47  /**
48   * The loaded display builder instance.
49   */
50  protected ?InstanceInterface $instance;
51
52  /**
53   * {@inheritdoc}
54   */
55  public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition): static {
56    $instance = parent::create($container, $configuration, $plugin_id, $plugin_definition);
57    $instance->entityTypeManager = $container->get('entity_type.manager');
58    $instance->themeRegistry = $container->get('theme.registry');
59    $instance->modules = $container->get('extension.list.module');
60
61    return $instance;
62  }
63
64  /**
65   * {@inheritdoc}
66   */
67  public function buildOptionsForm(&$form, FormStateInterface $form_state): void {
68    if ($form_state->get('section') !== 'display_builder') {
69      return;
70    }
71
72    $form['#title'] .= $this->t('Display Builder');
73    $form[DisplayBuildableInterface::PROFILE_PROPERTY] = $this->displayBuildable()->buildInstanceForm(FALSE);
74  }
75
76  /**
77   * {@inheritdoc}
78   */
79  public function submitOptionsForm(&$form, FormStateInterface $form_state): void {
80    if ($form_state->get('section') !== 'display_builder') {
81      return;
82    }
83
84    // @todo we should have always a fallback.
85    $profile_id = $form_state->getValue(DisplayBuildableInterface::PROFILE_PROPERTY, 'default');
86    $this->options[DisplayBuildableInterface::PROFILE_PROPERTY] = $profile_id;
87
88    if (empty($profile_id)) {
89      // If no Display Builder selected, we delete the related instance.
90      // @todo Do we move that to the View's EntityInterface::delete() method?
91      // @todo Also, when the changed are canceled from UI leaving the View
92      // without Display Builder.
93      $storage = $this->entityTypeManager->getStorage('display_builder_instance');
94      $storage->delete([$this->getInstance()]);
95
96      return;
97    }
98
99    $buildable = $this->displayBuildable();
100    $buildable->initInstanceIfMissing();
101  }
102
103  /**
104   * {@inheritdoc}
105   */
106  public function optionsSummary(&$categories, &$options): void {
107    $buildable = $this->displayBuildable();
108
109    if (!$this->isApplicable()) {
110      return;
111    }
112
113    $options['display_builder'] = [
114      'category' => 'other',
115      'title' => $this->t('Display Builder'),
116      'desc' => $this->t('Use display builder as output for this view.'),
117      'value' => $buildable->getProfile()?->label() ?? $this->t('Disabled'),
118    ];
119  }
120
121  /**
122   * {@inheritdoc}
123   */
124  public function preExecute(): void {
125    $buildable = $this->displayBuildable();
126
127    if (!$buildable->getProfile()) {
128      return;
129    }
130    // We alter the registry here instead of implementing
131    // hook_theme_registry_alter in order keep the alteration specific to each
132    // view.
133    $view = $this->view;
134    // Theme hook suggestion of the current view display.
135    $suggestion = \implode('__', ['views_view', $view->id(), $view->getDisplay()->getPluginId()]);
136    $entry = $this->buildThemeRegistryEntry();
137    $this->themeRegistry->getRuntime()->set($suggestion, $entry);
138  }
139
140  /**
141   * Build theme registry entry.
142   *
143   * @return array
144   *   A theme registry entry.
145   */
146  protected function buildThemeRegistryEntry(): array {
147    $theme_registry = $this->themeRegistry->get();
148    // Identical to views_view with a specific path.
149    $entry = $theme_registry['views_view'];
150    $entry['path'] = $this->modules->getPath('display_builder_views') . '/templates';
151
152    return $entry;
153  }
154
155  /**
156   * Gets the Display Builder instance.
157   *
158   * @return \Drupal\display_builder\InstanceInterface|null
159   *   A display builder instance.
160   */
161  protected function getInstance(): ?InstanceInterface {
162    if (!$this->displayBuildable()->getInstanceId()) {
163      return NULL;
164    }
165
166    if (!isset($this->instance)) {
167      $instance_id = $this->displayBuildable()->getInstanceId();
168      /** @var \Drupal\display_builder\InstanceInterface|null $instance */
169      $instance = $this->entityTypeManager->getStorage('display_builder_instance')->load($instance_id);
170      $this->instance = $instance;
171    }
172
173    return $this->instance;
174  }
175
176  /**
177   * If display builder can be applied to this display.
178   *
179   * @return bool
180   *   Applicable or not.
181   */
182  private function isApplicable(): bool {
183    $display = $this->view->getDisplay();
184    $display_definition = $display->getPluginDefinition();
185
186    if (!isset($display_definition['class'])) {
187      return FALSE;
188    }
189
190    // Do not include with feed and entity reference, as they have no output to
191    // apply a display builder to.
192    if ($display_definition['class'] === 'Drupal\views\Plugin\views\display\Feed') {
193      return FALSE;
194    }
195
196    if ($display_definition['class'] === 'Drupal\views\Plugin\views\display\EntityReference') {
197      return FALSE;
198    }
199
200    // @todo safer to not allow third party display?
201    // phpcs:disable
202    // if (str_contains($display_definition['class'], 'Drupal\views\Plugin\views\display')) {
203    //   return FALSE;
204    // }
205    // phpcs:enable
206
207    return TRUE;
208  }
209
210  /**
211   * Gets the display buildable manager.
212   *
213   * @return \Drupal\display_builder\DisplayBuildableInterface
214   *   The manager for display buildable.
215   */
216  private function displayBuildable(): DisplayBuildableInterface {
217    /** @var \Drupal\display_builder\DisplayBuildablePluginManager $manager */
218    $manager = \Drupal::service('plugin.manager.display_buildable');
219    /** @var \Drupal\display_builder\DisplayBuildableInterface $buildable */
220    $buildable = $manager->createInstance('view_display', ['extender' => $this]);
221
222    return $buildable;
223  }
224
225}