Code Coverage
 
Lines
Branches
Paths
Functions and Methods
Classes and Traits
Total
62.90% covered (warning)
62.90%
39 / 62
60.61% covered (warning)
60.61%
20 / 33
31.82% covered (danger)
31.82%
7 / 22
22.22% covered (danger)
22.22%
2 / 9
CRAP
0.00% covered (danger)
0.00%
0 / 1
DisplayExtender
62.90% covered (warning)
62.90%
39 / 62
60.61% covered (warning)
60.61%
20 / 33
31.82% covered (danger)
31.82%
7 / 22
22.22% covered (danger)
22.22%
2 / 9
160.78
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
57.14% covered (warning)
57.14%
8 / 14
66.67% covered (warning)
66.67%
6 / 9
16.67% covered (danger)
16.67%
1 / 6
0.00% covered (danger)
0.00%
0 / 1
19.47
 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    $buildable = $this->displayBuildable();
88
89    if (empty($profile_id)) {
90      // If no Display Builder selected, we delete the related instance.
91      // @todo Do we move that to the View's EntityInterface::delete() method?
92      // @todo Also, when the changed are canceled from UI leaving the View
93      // without Display Builder.
94      $storage = $this->entityTypeManager->getStorage('display_builder_instance');
95      $storage->delete([$this->getInstance()]);
96
97      return;
98    }
99
100    $buildable->initInstanceIfMissing();
101
102    // Save the profile in the instance if changed.
103    $instance = $this->getInstance();
104
105    if ($instance && $buildable->getProfile()->id() !== $profile_id) {
106      $instance->setProfile($profile_id);
107      $instance->save();
108    }
109  }
110
111  /**
112   * {@inheritdoc}
113   */
114  public function optionsSummary(&$categories, &$options): void {
115    $buildable = $this->displayBuildable();
116
117    if (!$this->isApplicable()) {
118      return;
119    }
120
121    $options['display_builder'] = [
122      'category' => 'other',
123      'title' => $this->t('Display Builder'),
124      'desc' => $this->t('Use display builder as output for this view.'),
125      'value' => $buildable->getProfile()?->label() ?? $this->t('Disabled'),
126    ];
127  }
128
129  /**
130   * {@inheritdoc}
131   */
132  public function preExecute(): void {
133    $buildable = $this->displayBuildable();
134
135    if (!$buildable->getProfile()) {
136      return;
137    }
138    // We alter the registry here instead of implementing
139    // hook_theme_registry_alter in order keep the alteration specific to each
140    // view.
141    $view = $this->view;
142    // Theme hook suggestion of the current view display.
143    $suggestion = \implode('__', ['views_view', $view->id(), $view->getDisplay()->getPluginId()]);
144    $entry = $this->buildThemeRegistryEntry();
145    $this->themeRegistry->getRuntime()->set($suggestion, $entry);
146  }
147
148  /**
149   * Build theme registry entry.
150   *
151   * @return array
152   *   A theme registry entry.
153   */
154  protected function buildThemeRegistryEntry(): array {
155    $theme_registry = $this->themeRegistry->get();
156    // Identical to views_view with a specific path.
157    $entry = $theme_registry['views_view'];
158    $entry['path'] = $this->modules->getPath('display_builder_views') . '/templates';
159
160    return $entry;
161  }
162
163  /**
164   * Gets the Display Builder instance.
165   *
166   * @return \Drupal\display_builder\InstanceInterface|null
167   *   A display builder instance.
168   */
169  protected function getInstance(): ?InstanceInterface {
170    if (!$this->displayBuildable()->getInstanceId()) {
171      return NULL;
172    }
173
174    if (!isset($this->instance)) {
175      $instance_id = $this->displayBuildable()->getInstanceId();
176      /** @var \Drupal\display_builder\InstanceInterface|null $instance */
177      $instance = $this->entityTypeManager->getStorage('display_builder_instance')->load($instance_id);
178      $this->instance = $instance;
179    }
180
181    return $this->instance;
182  }
183
184  /**
185   * If display builder can be applied to this display.
186   *
187   * @return bool
188   *   Applicable or not.
189   */
190  private function isApplicable(): bool {
191    $display = $this->view->getDisplay();
192    $display_definition = $display->getPluginDefinition();
193
194    if (!isset($display_definition['class'])) {
195      return FALSE;
196    }
197
198    // Do not include with feed and entity reference, as they have no output to
199    // apply a display builder to.
200    if ($display_definition['class'] === 'Drupal\views\Plugin\views\display\Feed') {
201      return FALSE;
202    }
203
204    if ($display_definition['class'] === 'Drupal\views\Plugin\views\display\EntityReference') {
205      return FALSE;
206    }
207
208    // @todo safer to not allow third party display?
209    // phpcs:disable
210    // if (str_contains($display_definition['class'], 'Drupal\views\Plugin\views\display')) {
211    //   return FALSE;
212    // }
213    // phpcs:enable
214
215    return TRUE;
216  }
217
218  /**
219   * Gets the display buildable manager.
220   *
221   * @return \Drupal\display_builder\DisplayBuildableInterface
222   *   The manager for display buildable.
223   */
224  private function displayBuildable(): DisplayBuildableInterface {
225    /** @var \Drupal\display_builder\DisplayBuildablePluginManager $manager */
226    $manager = \Drupal::service('plugin.manager.display_buildable');
227    /** @var \Drupal\display_builder\DisplayBuildableInterface $buildable */
228    $buildable = $manager->createInstance('view_display', ['extender' => $this]);
229
230    return $buildable;
231  }
232
233}