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