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}

Paths

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

DisplayExtender->buildOptionsForm
67  public function buildOptionsForm(&$form, FormStateInterface $form_state): void {
68    if ($form_state->get('section') !== 'display_builder') {
 
69      return;
67  public function buildOptionsForm(&$form, FormStateInterface $form_state): void {
68    if ($form_state->get('section') !== 'display_builder') {
 
72    $form['#title'] .= $this->t('Display Builder');
73    $form[DisplayBuildableInterface::PROFILE_PROPERTY] = $this->displayBuildable()->buildInstanceForm(FALSE);
74  }
DisplayExtender->buildThemeRegistryEntry
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  }
DisplayExtender->create
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  }
DisplayExtender->displayBuildable
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  }
DisplayExtender->getInstance
162    if (!$this->displayBuildable()->getInstanceId()) {
 
163      return NULL;
162    if (!$this->displayBuildable()->getInstanceId()) {
 
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;
 
173    return $this->instance;
174  }
162    if (!$this->displayBuildable()->getInstanceId()) {
 
166    if (!isset($this->instance)) {
 
173    return $this->instance;
174  }
DisplayExtender->isApplicable
183    $display = $this->view->getDisplay();
184    $display_definition = $display->getPluginDefinition();
185
186    if (!isset($display_definition['class'])) {
 
187      return FALSE;
183    $display = $this->view->getDisplay();
184    $display_definition = $display->getPluginDefinition();
185
186    if (!isset($display_definition['class'])) {
 
192    if ($display_definition['class'] === 'Drupal\views\Plugin\views\display\Feed') {
 
193      return FALSE;
183    $display = $this->view->getDisplay();
184    $display_definition = $display->getPluginDefinition();
185
186    if (!isset($display_definition['class'])) {
 
192    if ($display_definition['class'] === 'Drupal\views\Plugin\views\display\Feed') {
 
196    if ($display_definition['class'] === 'Drupal\views\Plugin\views\display\EntityReference') {
 
197      return FALSE;
183    $display = $this->view->getDisplay();
184    $display_definition = $display->getPluginDefinition();
185
186    if (!isset($display_definition['class'])) {
 
192    if ($display_definition['class'] === 'Drupal\views\Plugin\views\display\Feed') {
 
196    if ($display_definition['class'] === 'Drupal\views\Plugin\views\display\EntityReference') {
 
207    return TRUE;
208  }
DisplayExtender->optionsSummary
106  public function optionsSummary(&$categories, &$options): void {
107    $buildable = $this->displayBuildable();
108
109    if (!$this->isApplicable()) {
 
110      return;
106  public function optionsSummary(&$categories, &$options): void {
107    $buildable = $this->displayBuildable();
108
109    if (!$this->isApplicable()) {
 
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  }
DisplayExtender->preExecute
125    $buildable = $this->displayBuildable();
126
127    if (!$buildable->getProfile()) {
 
128      return;
125    $buildable = $this->displayBuildable();
126
127    if (!$buildable->getProfile()) {
 
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  }
DisplayExtender->submitOptionsForm
79  public function submitOptionsForm(&$form, FormStateInterface $form_state): void {
80    if ($form_state->get('section') !== 'display_builder') {
 
81      return;
79  public function submitOptionsForm(&$form, FormStateInterface $form_state): void {
80    if ($form_state->get('section') !== 'display_builder') {
 
85    $profile_id = $form_state->getValue(DisplayBuildableInterface::PROFILE_PROPERTY, 'default');
86    $this->options[DisplayBuildableInterface::PROFILE_PROPERTY] = $profile_id;
87
88    if (empty($profile_id)) {
 
93      $storage = $this->entityTypeManager->getStorage('display_builder_instance');
94      $storage->delete([$this->getInstance()]);
95
96      return;
79  public function submitOptionsForm(&$form, FormStateInterface $form_state): void {
80    if ($form_state->get('section') !== 'display_builder') {
 
85    $profile_id = $form_state->getValue(DisplayBuildableInterface::PROFILE_PROPERTY, 'default');
86    $this->options[DisplayBuildableInterface::PROFILE_PROPERTY] = $profile_id;
87
88    if (empty($profile_id)) {
 
99    $buildable = $this->displayBuildable();
100    $buildable->initInstanceIfMissing();
101  }