Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
Total | |
59.22% |
61 / 103 |
|
33.33% |
7 / 21 |
CRAP | |
0.00% |
0 / 1 |
DisplayExtender | |
59.22% |
61 / 103 |
|
33.33% |
7 / 21 |
135.90 | |
0.00% |
0 / 1 |
create | |
100.00% |
6 / 6 |
|
100.00% |
1 / 1 |
1 | |||
getPrefix | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
buildOptionsForm | |
75.00% |
3 / 4 |
|
0.00% |
0 / 1 |
2.06 | |||
submitOptionsForm | |
75.00% |
6 / 8 |
|
0.00% |
0 / 1 |
3.14 | |||
optionsSummary | |
87.50% |
7 / 8 |
|
0.00% |
0 / 1 |
2.01 | |||
preExecute | |
0.00% |
0 / 6 |
|
0.00% |
0 / 1 |
6 | |||
getContextRequirement | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
getBuilderUrl | |
0.00% |
0 / 5 |
|
0.00% |
0 / 1 |
2 | |||
checkInstanceId | |
85.71% |
6 / 7 |
|
0.00% |
0 / 1 |
2.01 | |||
getUrlFromInstanceId | |
75.00% |
3 / 4 |
|
0.00% |
0 / 1 |
2.06 | |||
getDisplayUrlFromInstanceId | |
0.00% |
0 / 4 |
|
0.00% |
0 / 1 |
6 | |||
getProfile | |
75.00% |
6 / 8 |
|
0.00% |
0 / 1 |
3.14 | |||
getInstanceId | |
66.67% |
2 / 3 |
|
0.00% |
0 / 1 |
2.15 | |||
initInstanceIfMissing | |
100.00% |
5 / 5 |
|
100.00% |
1 / 1 |
2 | |||
getInitialSources | |
100.00% |
4 / 4 |
|
100.00% |
1 / 1 |
2 | |||
getInitialContext | |
100.00% |
4 / 4 |
|
100.00% |
1 / 1 |
1 | |||
getSources | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
saveSources | |
0.00% |
0 / 7 |
|
0.00% |
0 / 1 |
2 | |||
buildThemeRegistryEntry | |
0.00% |
0 / 4 |
|
0.00% |
0 / 1 |
2 | |||
isApplicable | |
66.67% |
6 / 9 |
|
0.00% |
0 / 1 |
4.59 | |||
getInstance | |
0.00% |
0 / 4 |
|
0.00% |
0 / 1 |
6 |
1 | <?php |
2 | |
3 | declare(strict_types=1); |
4 | |
5 | namespace Drupal\display_builder_views\Plugin\views\display_extender; |
6 | |
7 | use Drupal\Core\Extension\ModuleExtensionList; |
8 | use Drupal\Core\Form\FormStateInterface; |
9 | use Drupal\Core\Plugin\Context\EntityContext; |
10 | use Drupal\Core\StringTranslation\TranslatableMarkup; |
11 | use Drupal\Core\Theme\Registry; |
12 | use Drupal\Core\Url; |
13 | use Drupal\display_builder\ConfigFormBuilderInterface; |
14 | use Drupal\display_builder\DisplayBuildableInterface; |
15 | use Drupal\display_builder\DisplayBuilderHelpers; |
16 | use Drupal\display_builder\InstanceInterface; |
17 | use Drupal\display_builder\ProfileInterface; |
18 | use Drupal\ui_patterns\Plugin\Context\RequirementsContext; |
19 | use Drupal\views\Attribute\ViewsDisplayExtender; |
20 | use Drupal\views\Plugin\views\display_extender\DisplayExtenderPluginBase; |
21 | use 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 | )] |
34 | final 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 | } |