Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
59.22% covered (warning)
59.22%
61 / 103
33.33% covered (danger)
33.33%
7 / 21
CRAP
0.00% covered (danger)
0.00%
0 / 1
DisplayExtender
59.22% covered (warning)
59.22%
61 / 103
33.33% covered (danger)
33.33%
7 / 21
135.90
0.00% covered (danger)
0.00%
0 / 1
 create
100.00% covered (success)
100.00%
6 / 6
100.00% covered (success)
100.00%
1 / 1
1
 getPrefix
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
0.00% covered (danger)
0.00%
0 / 1
2.06
 submitOptionsForm
75.00% covered (warning)
75.00%
6 / 8
0.00% covered (danger)
0.00%
0 / 1
3.14
 optionsSummary
87.50% covered (warning)
87.50%
7 / 8
0.00% covered (danger)
0.00%
0 / 1
2.01
 preExecute
0.00% covered (danger)
0.00%
0 / 6
0.00% covered (danger)
0.00%
0 / 1
6
 getContextRequirement
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getBuilderUrl
0.00% covered (danger)
0.00%
0 / 5
0.00% covered (danger)
0.00%
0 / 1
2
 checkInstanceId
85.71% covered (warning)
85.71%
6 / 7
0.00% covered (danger)
0.00%
0 / 1
2.01
 getUrlFromInstanceId
75.00% covered (warning)
75.00%
3 / 4
0.00% covered (danger)
0.00%
0 / 1
2.06
 getDisplayUrlFromInstanceId
0.00% covered (danger)
0.00%
0 / 4
0.00% covered (danger)
0.00%
0 / 1
6
 getProfile
75.00% covered (warning)
75.00%
6 / 8
0.00% covered (danger)
0.00%
0 / 1
3.14
 getInstanceId
66.67% covered (warning)
66.67%
2 / 3
0.00% covered (danger)
0.00%
0 / 1
2.15
 initInstanceIfMissing
100.00% covered (success)
100.00%
5 / 5
100.00% covered (success)
100.00%
1 / 1
2
 getInitialSources
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
1 / 1
2
 getInitialContext
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
1 / 1
1
 getSources
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 saveSources
0.00% covered (danger)
0.00%
0 / 7
0.00% covered (danger)
0.00%
0 / 1
2
 buildThemeRegistryEntry
0.00% covered (danger)
0.00%
0 / 4
0.00% covered (danger)
0.00%
0 / 1
2
 isApplicable
66.67% covered (warning)
66.67%
6 / 9
0.00% covered (danger)
0.00%
0 / 1
4.59
 getInstance
0.00% covered (danger)
0.00%
0 / 4
0.00% covered (danger)
0.00%
0 / 1
6
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\Plugin\Context\EntityContext;
10use Drupal\Core\StringTranslation\TranslatableMarkup;
11use Drupal\Core\Theme\Registry;
12use Drupal\Core\Url;
13use Drupal\display_builder\ConfigFormBuilderInterface;
14use Drupal\display_builder\DisplayBuildableInterface;
15use Drupal\display_builder\DisplayBuilderHelpers;
16use Drupal\display_builder\InstanceInterface;
17use Drupal\display_builder\ProfileInterface;
18use Drupal\ui_patterns\Plugin\Context\RequirementsContext;
19use Drupal\views\Attribute\ViewsDisplayExtender;
20use Drupal\views\Plugin\views\display_extender\DisplayExtenderPluginBase;
21use Symfony\Component\DependencyInjection\ContainerInterface;
22
23/**
24 * Styles display extender plugin.
25 *
26 * @ingroup views_display_extender_plugins
27 */
28#[ViewsDisplayExtender(
29  id: 'display_builder',
30  title: new TranslatableMarkup('Display Builder'),
31  help: new TranslatableMarkup('Use display builder as output for this view.'),
32  no_ui: FALSE,
33)]
34final class DisplayExtender extends DisplayExtenderPluginBase implements DisplayBuildableInterface {
35
36  /**
37   * The config form builder for Display Builder.
38   *
39   * @var \Drupal\display_builder\ConfigFormBuilderInterface
40   */
41  protected $configFormBuilder;
42
43  /**
44   * The entity type interface.
45   *
46   * @var \Drupal\Core\Entity\EntityTypeManagerInterface
47   */
48  protected $entityTypeManager;
49
50  /**
51   * The theme registry.
52   */
53  protected Registry $themeRegistry;
54
55  /**
56   * The list of modules.
57   */
58  protected ModuleExtensionList $modules;
59
60  /**
61   * The loaded display builder instance.
62   */
63  protected ?InstanceInterface $instance;
64
65  /**
66   * {@inheritdoc}
67   */
68  public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition): self {
69    $instance = parent::create($container, $configuration, $plugin_id, $plugin_definition);
70    $instance->configFormBuilder = $container->get('display_builder.config_form_builder');
71    $instance->entityTypeManager = $container->get('entity_type.manager');
72    $instance->themeRegistry = $container->get('theme.registry');
73    $instance->modules = $container->get('extension.list.module');
74
75    return $instance;
76  }
77
78  /**
79   * {@inheritdoc}
80   */
81  public static function getPrefix(): string {
82    return 'views__';
83  }
84
85  /**
86   * {@inheritdoc}
87   */
88  public function buildOptionsForm(&$form, FormStateInterface $form_state): void {
89    if ($form_state->get('section') !== 'display_builder') {
90      return;
91    }
92
93    $form['#title'] .= $this->t('Display Builder');
94    $form[ConfigFormBuilderInterface::PROFILE_PROPERTY] = $this->configFormBuilder->build($this, FALSE);
95  }
96
97  /**
98   * {@inheritdoc}
99   */
100  public function submitOptionsForm(&$form, FormStateInterface $form_state): void {
101    if ($form_state->get('section') !== 'display_builder') {
102      return;
103    }
104
105    // @todo we should have always a fallback.
106    $display_builder_config = $form_state->getValue(ConfigFormBuilderInterface::PROFILE_PROPERTY, 'default');
107    $this->options[ConfigFormBuilderInterface::PROFILE_PROPERTY] = $display_builder_config;
108
109    if (!empty($display_builder_config)) {
110      $this->initInstanceIfMissing();
111
112      return;
113    }
114
115    // If no Display Builder selected, we delete the related instance.
116    // @todo Do we move that to the View's EntityInterface::delete() method?
117    // @todo Also, when the changed are canceled from UI leaving the View
118    // without Display Builder.
119    $this->entityTypeManager->getStorage('display_builder_instance')->delete([$this->getInstance()]);
120  }
121
122  /**
123   * {@inheritdoc}
124   */
125  public function optionsSummary(&$categories, &$options): void {
126    if (!$this->isApplicable()) {
127      return;
128    }
129
130    $options['display_builder'] = [
131      'category' => 'other',
132      'title' => $this->t('Display Builder'),
133      'desc' => $this->t('Use display builder as output for this view.'),
134      'value' => $this->getProfile()?->label() ?? $this->t('Disabled'),
135    ];
136  }
137
138  /**
139   * {@inheritdoc}
140   */
141  public function preExecute(): void {
142    if (!$this->getProfile()) {
143      return;
144    }
145    // We alter the registry here instead of implementing
146    // hook_theme_registry_alter in order keep the alteration specific to each
147    // view.
148    $view = $this->view;
149    // Theme hook suggestion of the current view display.
150    $suggestion = \implode('__', ['views_view', $view->id(), $view->getDisplay()->getPluginId()]);
151    $entry = $this->buildThemeRegistryEntry();
152    $this->themeRegistry->getRuntime()->set($suggestion, $entry);
153  }
154
155  /**
156   * {@inheritdoc}
157   */
158  public static function getContextRequirement(): string {
159    // @see \Drupal\ui_patterns_views\Plugin\UiPatterns\Source\ViewRowsSource.
160    return 'views:style';
161  }
162
163  /**
164   * {@inheritdoc}
165   */
166  public function getBuilderUrl(): Url {
167    $params = [
168      'view' => $this->view->id(),
169      'display' => $this->view->current_display,
170    ];
171
172    return Url::fromRoute('display_builder_views.views.manage', $params);
173  }
174
175  /**
176   * {@inheritdoc}
177   */
178  public static function checkInstanceId(string $instance_id): ?array {
179    if (!\str_starts_with($instance_id, DisplayExtender::getPrefix())) {
180      return NULL;
181    }
182    [, $view, $display] = \explode('__', $instance_id);
183
184    return [
185      'view' => $view,
186      'display' => $display,
187    ];
188  }
189
190  /**
191   * {@inheritdoc}
192   */
193  public static function getUrlFromInstanceId(string $instance_id): Url {
194    $params = self::checkInstanceId($instance_id);
195
196    if (!$params) {
197      // Fallback to the list of instances.
198      return Url::fromRoute('entity.display_builder_instance.collection');
199    }
200
201    return Url::fromRoute('display_builder_views.views.manage', $params);
202  }
203
204  /**
205   * {@inheritdoc}
206   */
207  public static function getDisplayUrlFromInstanceId(string $instance_id): Url {
208    $params = self::checkInstanceId($instance_id);
209
210    if (!$params) {
211      // Fallback to the list of instances.
212      return Url::fromRoute('entity.display_builder_instance.collection');
213    }
214
215    return Url::fromRoute('entity.view.edit_form', $params);
216  }
217
218  /**
219   * {@inheritdoc}
220   */
221  public function getProfile(): ?ProfileInterface {
222    if (!isset($this->options[ConfigFormBuilderInterface::PROFILE_PROPERTY])) {
223      return NULL;
224    }
225    $display_builder_id = $this->options[ConfigFormBuilderInterface::PROFILE_PROPERTY];
226
227    if (empty($display_builder_id)) {
228      return NULL;
229    }
230    $storage = $this->entityTypeManager->getStorage('display_builder_profile');
231
232    /** @var \Drupal\display_builder\ProfileInterface $display_builder */
233    $display_builder = $storage->load($display_builder_id);
234
235    return $display_builder;
236  }
237
238  /**
239   * {@inheritdoc}
240   */
241  public function getInstanceId(): ?string {
242    if (!$this->view) {
243      return NULL;
244    }
245
246    return \sprintf('%s%s__%s', self::getPrefix(), $this->view->id(), $this->view->current_display);
247  }
248
249  /**
250   * {@inheritdoc}
251   */
252  public function initInstanceIfMissing(): void {
253    /** @var \Drupal\display_builder\InstanceStorage $storage */
254    $storage = $this->entityTypeManager->getStorage('display_builder_instance');
255
256    /** @var \Drupal\display_builder\InstanceInterface $instance */
257    $instance = $storage->load($this->getInstanceId());
258
259    if (!$instance) {
260      $instance = $storage->createFromImplementation($this);
261      $instance->save();
262    }
263  }
264
265  /**
266   * {@inheritdoc}
267   */
268  public function getInitialSources(): array {
269    // Get the sources stored in config.
270    $sources = $this->getSources();
271
272    if (empty($sources)) {
273      // Fallback to a fixture mimicking the standard view layout.
274      $sources = DisplayBuilderHelpers::getFixtureDataFromExtension('display_builder_views', 'default_view');
275    }
276
277    return $sources;
278  }
279
280  /**
281   * {@inheritdoc}
282   */
283  public function getInitialContext(): array {
284    $contexts = [];
285    // Mark for usage with views.
286    $contexts = RequirementsContext::addToContext([self::getContextRequirement()], $contexts);
287    // Add view entity that we need in our sources or even UI Patterns Views
288    // sources.
289    $contexts['ui_patterns_views:view_entity'] = EntityContext::fromEntity($this->view->storage);
290
291    return $contexts;
292  }
293
294  /**
295   * {@inheritdoc}
296   */
297  public function getSources(): array {
298    return $this->options[ConfigFormBuilderInterface::SOURCES_PROPERTY] ?? [];
299  }
300
301  /**
302   * {@inheritdoc}
303   */
304  public function saveSources(): void {
305    $sources = $this->getInstance()->getCurrentState();
306    $displays = $this->view->storage->get('display');
307    $display_id = $this->view->current_display;
308    // It is risky to alter a View like that. We need to be careful to not
309    // break the storage integrity, but we didn't find a better way.
310    $displays[$display_id]['display_options']['display_extenders']['display_builder']['sources'] = $sources;
311    $this->view->storage->set('display', $displays);
312    $this->view->storage->save();
313    // @todo Test if we still need to invalidate the cache manually here.
314    $this->view->storage->invalidateCaches();
315  }
316
317  /**
318   * Build theme registry entry.
319   *
320   * @return array
321   *   A theme registry entry.
322   */
323  protected function buildThemeRegistryEntry(): array {
324    $theme_registry = $this->themeRegistry->get();
325    // Identical to views_view with a specific path.
326    $entry = $theme_registry['views_view'];
327    $entry['path'] = $this->modules->getPath('display_builder_views') . '/templates';
328
329    return $entry;
330  }
331
332  /**
333   * If display builder can be applied to this display.
334   *
335   * @return bool
336   *   Applicable or not.
337   */
338  private function isApplicable(): bool {
339    $display = $this->view->getDisplay();
340    $display_definition = $display->getPluginDefinition();
341
342    if (!isset($display_definition['class'])) {
343      return FALSE;
344    }
345
346    // Do not include with feed and entity reference, as they have no output to
347    // apply a display builder to.
348    if ($display_definition['class'] === 'Drupal\views\Plugin\views\display\Feed') {
349      return FALSE;
350    }
351
352    if ($display_definition['class'] === 'Drupal\views\Plugin\views\display\EntityReference') {
353      return FALSE;
354    }
355
356    // @todo safer to not allow third party display?
357    // phpcs:disable
358    // if (str_contains($display_definition['class'], 'Drupal\views\Plugin\views\display')) {
359    //   return FALSE;
360    // }
361    // phpcs:enable
362
363    return TRUE;
364  }
365
366  /**
367   * Gets the Display Builder instance.
368   *
369   * @return \Drupal\display_builder\InstanceInterface|null
370   *   A display builder instance entity.
371   */
372  private function getInstance(): ?InstanceInterface {
373    if (!isset($this->instance)) {
374      /** @var \Drupal\display_builder\InstanceInterface|null $instance */
375      $instance = $this->entityTypeManager->getStorage('display_builder_instance')->load($this->getInstanceId());
376      $this->instance = $instance;
377    }
378
379    return $this->instance;
380  }
381
382}