Code Coverage
 
Lines
Branches
Paths
Functions and Methods
Classes and Traits
Total
83.62% covered (warning)
83.62%
97 / 116
80.33% covered (warning)
80.33%
49 / 61
31.25% covered (danger)
31.25%
15 / 48
52.63% covered (warning)
52.63%
10 / 19
CRAP
0.00% covered (danger)
0.00%
0 / 1
LayoutSource
83.62% covered (warning)
83.62%
97 / 116
80.33% covered (warning)
80.33%
49 / 61
31.25% covered (danger)
31.25%
15 / 48
57.89% covered (warning)
57.89%
11 / 19
457.14
0.00% covered (danger)
0.00%
0 / 1
 __construct
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
 create
100.00% covered (success)
100.00%
17 / 17
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
 defaultSettings
0.00% covered (danger)
0.00%
0 / 6
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
 getPropValue
83.33% covered (warning)
83.33%
10 / 12
81.82% covered (warning)
81.82%
9 / 11
14.29% covered (danger)
14.29%
1 / 7
0.00% covered (danger)
0.00%
0 / 1
20.74
 settingsForm
100.00% covered (success)
100.00%
8 / 8
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
 settingsSummary
0.00% covered (danger)
0.00%
0 / 1
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
 getChoices
100.00% covered (success)
100.00%
14 / 14
100.00% covered (success)
100.00%
9 / 9
16.67% covered (danger)
16.67%
1 / 6
100.00% covered (success)
100.00%
1 / 1
13.26
 getChoiceSettings
0.00% covered (danger)
0.00%
0 / 3
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
 getChoice
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
 calculateDependencies
100.00% covered (success)
100.00%
21 / 21
91.67% covered (success)
91.67%
11 / 12
8.33% covered (danger)
8.33%
1 / 12
100.00% covered (success)
100.00%
1 / 1
24.26
 getSlotDefinitions
87.50% covered (warning)
87.50%
7 / 8
83.33% covered (warning)
83.33%
5 / 6
25.00% covered (danger)
25.00%
1 / 4
0.00% covered (danger)
0.00%
0 / 1
6.80
 getSlotValues
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
 getSlotValue
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
 setSlotValue
100.00% covered (success)
100.00%
2 / 2
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
 setSlotRenderable
0.00% covered (danger)
0.00%
0 / 2
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
 getSlotPath
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
 settingsFormPropsOnly
100.00% covered (success)
100.00%
8 / 8
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
 getLayout
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
 getExtensionType
40.00% covered (danger)
40.00%
2 / 5
40.00% covered (danger)
40.00%
2 / 5
33.33% covered (danger)
33.33%
1 / 3
0.00% covered (danger)
0.00%
0 / 1
5.67
1<?php
2
3declare(strict_types=1);
4
5namespace Drupal\display_builder\Plugin\UiPatterns\Source;
6
7use Drupal\Component\Plugin\Definition\PluginDefinitionInterface;
8use Drupal\Component\Render\MarkupInterface;
9use Drupal\Core\Extension\ModuleHandlerInterface;
10use Drupal\Core\Extension\ThemeHandlerInterface;
11use Drupal\Core\Form\FormStateInterface;
12use Drupal\Core\Layout\LayoutInterface;
13use Drupal\Core\Layout\LayoutPluginManagerInterface;
14use Drupal\Core\Plugin\Context\ContextRepositoryInterface;
15use Drupal\Core\Plugin\PluginFormInterface;
16use Drupal\Core\Routing\RouteMatchInterface;
17use Drupal\Core\StringTranslation\TranslatableMarkup;
18use Drupal\Core\Utility\Token;
19use Drupal\display_builder\SourceWithSlotsInterface;
20use Drupal\ui_patterns\Attribute\Source;
21use Drupal\ui_patterns\Element\ComponentElementBuilder;
22use Drupal\ui_patterns\Entity\SampleEntityGeneratorInterface;
23use Drupal\ui_patterns\PropTypePluginManager;
24use Drupal\ui_patterns\SourcePluginBase;
25use Drupal\ui_patterns\SourcePluginManager;
26use Drupal\ui_patterns\SourceWithChoicesInterface;
27use Drupal\ui_patterns\UiPatternsNormalizerInterface;
28use Symfony\Component\DependencyInjection\ContainerInterface;
29
30/**
31 * Plugin implementation of the source.
32 */
33#[Source(
34  id: 'layout',
35  label: new TranslatableMarkup('Layout'),
36  prop_types: ['slot'],
37  context_requirements: ['layout_discovery'],
38)]
39class LayoutSource extends SourcePluginBase implements SourceWithChoicesInterface, SourceWithSlotsInterface {
40
41  /**
42   * {@inheritdoc}
43   */
44  public function __construct(
45    array $configuration,
46    $plugin_id,
47    $plugin_definition,
48    PropTypePluginManager $propTypeManager,
49    ContextRepositoryInterface $contextRepository,
50    RouteMatchInterface $routeMatch,
51    SampleEntityGeneratorInterface $sampleEntityGenerator,
52    ModuleHandlerInterface $moduleHandler,
53    Token $token,
54    UiPatternsNormalizerInterface $normalizer,
55    protected ComponentElementBuilder $componentElementBuilder,
56    protected LayoutPluginManagerInterface $layoutManager,
57    protected SourcePluginManager $sourceManager,
58    protected ThemeHandlerInterface $themeHandler,
59  ) {
60    parent::__construct($configuration, $plugin_id, $plugin_definition, $propTypeManager, $contextRepository, $routeMatch, $sampleEntityGenerator, $moduleHandler, $token, $normalizer);
61  }
62
63  /**
64   * {@inheritdoc}
65   */
66  public static function create(
67    ContainerInterface $container,
68    array $configuration,
69    $plugin_id,
70    $plugin_definition,
71  ): static {
72    $instance = new static(
73      $configuration,
74      $plugin_id,
75      $plugin_definition,
76      $container->get('plugin.manager.ui_patterns_prop_type'),
77      $container->get('context.repository'),
78      $container->get('current_route_match'),
79      $container->get('ui_patterns.sample_entity_generator'),
80      $container->get('module_handler'),
81      $container->get('token'),
82      $container->get('ui_patterns.normalizer'),
83      $container->get('ui_patterns.component_element_builder'),
84      $container->get('plugin.manager.core.layout'),
85      $container->get('plugin.manager.ui_patterns_source'),
86      $container->get('theme_handler'),
87    );
88
89    return $instance;
90  }
91
92  /**
93   * {@inheritdoc}
94   */
95  public function defaultSettings(): array {
96    $layout = $this->getLayout();
97
98    return [
99      'layout_id' => NULL,
100      'settings' => $layout->defaultConfiguration() ?? [],
101      'regions' => [],
102    ];
103  }
104
105  /**
106   * {@inheritdoc}
107   */
108  public function getPropValue(): mixed {
109    $layout = $this->getLayout();
110
111    if (!$layout) {
112      return [];
113    }
114
115    $regions = [];
116
117    foreach ($this->configuration['settings']['regions'] ?? [] as $region_id => $region) {
118      foreach ($region as $source) {
119        $content = $this->componentElementBuilder->buildSource([], 'content', [], $source, $this->configuration['contexts'] ?? []) ?? [];
120        $content = $content['#slots']['content'][0] ?? [];
121
122        // An empty render array is enough to cancel the rendering of the full
123        // layout plugins, so let's remove them from the renderable.
124        if (empty($content)) {
125          continue;
126        }
127        $regions[$region_id][] = $content;
128      }
129    }
130
131    return $layout->build($regions);
132  }
133
134  /**
135   * {@inheritdoc}
136   */
137  public function settingsForm(array $form, FormStateInterface $form_state): array {
138    // Careful with the configuration/settings/settings hierarchy where:
139    // - $this->configuration has everything UI Patterns needs to work properly
140    //   (prop_id, prop_definition, contexts..)
141    // - $this->configuration['settings'] is the source data from the source
142    //   tree
143    // - $this->configuration['settings']['settings'] is the layout plugin
144    //   configuration.
145    $form = parent::settingsForm($form, $form_state);
146    $form['#tree'] = TRUE;
147
148    // We are not implementing a slot sources form for $forms['regions'],
149    // because Display Builder doesn't need layout regions to be available in
150    // the component form.
151    // @todo However, for compatibility with UI Patterns ecosystem, it would be
152    // better to implement it anyway. It will be removed by
153    // ::settingsFormPropsOnly() anyway.
154    $form['regions'] = [];
155    $layout = $this->getLayout();
156
157    if ($layout instanceof PluginFormInterface) {
158      $form['settings'] = $layout->buildConfigurationForm($form, $form_state);
159      // Hide administrative label textfield.
160      $form['settings']['label']['#type'] = 'hidden';
161    }
162
163    return $form;
164  }
165
166  /**
167   * {@inheritdoc}
168   */
169  public function settingsSummary(): array {
170    // @todo settings summary
171    return [];
172  }
173
174  /**
175   * {@inheritdoc}
176   */
177  public function getChoices(): array {
178    $definitions = $this->layoutManager->getGroupedDefinitions();
179    $choices = [];
180
181    foreach ($definitions as $group_id => $group) {
182      foreach ($group as $layout_id => $definition) {
183        /** @var \Drupal\Core\Layout\LayoutDefinition $definition */
184        $choice = [
185          'label' => $definition->getLabel(),
186          'original_id' => $layout_id,
187          'group' => $group_id,
188          'provider' => $definition,
189        ];
190
191        if ($choice['label'] instanceof MarkupInterface) {
192          $choice['label'] = (string) $choice['label'];
193        }
194        $choices[$layout_id] = $choice;
195      }
196    }
197
198    return $choices;
199  }
200
201  /**
202   * {@inheritdoc}
203   */
204  public function getChoiceSettings(string $choice_id): array {
205    return [
206      'layout_id' => $choice_id,
207    ];
208  }
209
210  /**
211   * {@inheritdoc}
212   */
213  public function getChoice(array $settings): string {
214    return $settings['layout_id'] ?? '';
215  }
216
217  /**
218   * {@inheritdoc}
219   */
220  public function calculateDependencies(): array {
221    $dependencies = parent::calculateDependencies();
222    $layout = $this->getLayout();
223    $provider = [];
224
225    // 1. Dependency of plugin.manager.core.layout.
226    $provider['module'][] = 'layout_discovery';
227
228    // 2. Layout provider extension (module or theme)
229    $definition = $layout->getPluginDefinition();
230    $provider = ($definition instanceof PluginDefinitionInterface) ? $definition->getProvider() : (string) ($definition['provider'] ?? '');
231    $extension_type = $this->getExtensionType($provider);
232    $dependencies[$extension_type][] = $provider;
233
234    // 3. Layout plugin dependencies.
235    SourcePluginBase::mergeConfigDependencies(
236      $dependencies,
237      $layout->calculateDependencies()
238    );
239
240    // 4. Sources in slots.
241    $slot_definition = ['ui_patterns' => ['type_definition' => $this->sourceManager->getSlotPropType()]];
242
243    foreach ($this->getSlotValues() as $slot_id => $slot) {
244      foreach ($slot as $source) {
245        if ($source = $this->sourceManager->getSource($slot_id, $slot_definition, $source, [])) {
246          SourcePluginBase::mergeConfigDependencies(
247            $dependencies,
248            $source->calculateDependencies()
249          );
250        }
251      }
252    }
253
254    return $dependencies;
255  }
256
257  /**
258   * {@inheritdoc}
259   */
260  public function getSlotDefinitions(): array {
261    $slots = [];
262    $layout_id = $this->configuration['settings']['layout_id'] ?? NULL;
263
264    if (!$layout_id) {
265      return [];
266    }
267    $definition = $this->layoutManager->getDefinition($layout_id);
268
269    foreach ($definition->getRegions() as $region_id => $region) {
270      $slots[$region_id]['title'] = (string) $region['label'];
271    }
272
273    return $slots;
274  }
275
276  /**
277   * {@inheritdoc}
278   */
279  public function getSlotValues(): array {
280    return $this->configuration['settings']['regions'] ?? [];
281  }
282
283  /**
284   * {@inheritdoc}
285   */
286  public function getSlotValue(string $slot_id): array {
287    return $this->configuration['settings']['regions'][$slot_id] ?? [];
288  }
289
290  /**
291   * {@inheritdoc}
292   */
293  public function setSlotValue(string $slot_id, array $slot): array {
294    $this->configuration['settings']['regions'][$slot_id] = $slot;
295
296    return $this->configuration['settings'];
297  }
298
299  /**
300   * {@inheritdoc}
301   */
302  public function setSlotRenderable(array $build, string $slot_id, array $slot): array {
303    $build[$slot_id] = $slot;
304
305    return $build;
306  }
307
308  /**
309   * {@inheritdoc}
310   */
311  public static function getSlotPath(string $slot_id): array {
312    return ['regions', $slot_id];
313  }
314
315  /**
316   * {@inheritdoc}
317   */
318  public function settingsFormPropsOnly(array $form, FormStateInterface $form_state): array {
319    $form = $this->settingsForm($form, $form_state);
320    unset($form['regions']);
321    $layout = $this->getLayout();
322    $form['layout_id'] = [
323      '#type' => 'hidden',
324      '#value' => $layout->getPluginId(),
325    ];
326
327    return $form;
328  }
329
330  /**
331   * Get layout plugin.
332   *
333   * @return \Drupal\Core\Layout\LayoutInterface|null
334   *   The layout plugin.
335   */
336  private function getLayout(): ?LayoutInterface {
337    $layout_id = $this->configuration['settings']['layout_id'] ?? NULL;
338
339    if (!$layout_id) {
340      return NULL;
341    }
342
343    return $this->layoutManager->createInstance($layout_id, $this->configuration['settings']['settings'] ?? []);
344  }
345
346  /**
347   * Get extension type (theme or module).
348   *
349   * @param string $extension
350   *   Extension (module or theme) machine name.
351   *
352   * @return string
353   *   Extension type.
354   */
355  private function getExtensionType(string $extension): string {
356    if ($this->moduleHandler->moduleExists($extension)) {
357      return 'module';
358    }
359
360    if ($this->themeHandler->themeExists($extension)) {
361      return 'theme';
362    }
363
364    return '';
365  }
366
367}