Code Coverage
 
Lines
Branches
Paths
Functions and Methods
Classes and Traits
Total
87.59% covered (warning)
87.59%
120 / 137
82.29% covered (warning)
82.29%
79 / 96
50.00% covered (danger)
50.00%
32 / 64
57.89% covered (warning)
57.89%
11 / 19
CRAP
0.00% covered (danger)
0.00%
0 / 1
ComponentSource
87.59% covered (warning)
87.59%
120 / 137
82.29% covered (warning)
82.29%
79 / 96
50.00% covered (danger)
50.00%
32 / 64
57.89% covered (warning)
57.89%
11 / 19
404.12
0.00% covered (danger)
0.00%
0 / 1
 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
 getSlotDefinitions
57.14% covered (warning)
57.14%
4 / 7
50.00% covered (danger)
50.00%
3 / 6
33.33% covered (danger)
33.33%
1 / 3
0.00% covered (danger)
0.00%
0 / 1
5.67
 getSlotValues
100.00% covered (success)
100.00%
5 / 5
100.00% covered (success)
100.00%
6 / 6
25.00% covered (danger)
25.00%
1 / 4
100.00% covered (success)
100.00%
1 / 1
6.80
 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
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
 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
 settingsSummary
91.67% covered (success)
91.67%
11 / 12
91.67% covered (success)
91.67%
11 / 12
36.36% covered (danger)
36.36%
4 / 11
0.00% covered (danger)
0.00%
0 / 1
15.28
 settingsFormPropsOnly
93.75% covered (success)
93.75%
15 / 16
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
 getGroup
0.00% covered (danger)
0.00%
0 / 5
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
 getComponentMetadata
100.00% covered (success)
100.00%
29 / 29
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
 buildVariantSummary
87.50% covered (warning)
87.50%
7 / 8
85.71% covered (warning)
85.71%
6 / 7
50.00% covered (danger)
50.00%
3 / 6
0.00% covered (danger)
0.00%
0 / 1
6.00
 buildPropsSummary
100.00% covered (success)
100.00%
13 / 13
100.00% covered (success)
100.00%
8 / 8
60.00% covered (warning)
60.00%
3 / 5
100.00% covered (success)
100.00%
1 / 1
5.02
 processProperty
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
2
 isUiStyleAttribute
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
2
 formatUiStyleSummary
83.33% covered (warning)
83.33%
5 / 6
66.67% covered (warning)
66.67%
4 / 6
33.33% covered (danger)
33.33%
1 / 3
0.00% covered (danger)
0.00%
0 / 1
5.67
 processStandardProperty
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
 normalizeValue
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
11 / 11
66.67% covered (warning)
66.67%
4 / 6
100.00% covered (success)
100.00%
1 / 1
5.93
 flattenArrayToString
63.64% covered (warning)
63.64%
7 / 11
61.54% covered (warning)
61.54%
8 / 13
28.57% covered (danger)
28.57%
2 / 7
0.00% covered (danger)
0.00%
0 / 1
19.12
1<?php
2
3declare(strict_types=1);
4
5namespace Drupal\display_builder\Plugin\UiPatterns\Source;
6
7use Drupal\Core\Form\FormStateInterface;
8use Drupal\Core\StringTranslation\TranslatableMarkup;
9use Drupal\display_builder\SourceWithSlotsInterface;
10use Drupal\ui_patterns\Attribute\Source;
11use Drupal\ui_patterns\Plugin\UiPatterns\Source\ComponentSource as UpstreamComponentSource;
12
13/**
14 * Plugin implementation of the source.
15 */
16#[Source(
17  id: 'component',
18  label: new TranslatableMarkup('Component'),
19  description: new TranslatableMarkup('Add a Component'),
20  prop_types: ['slot']
21)]
22class ComponentSource extends UpstreamComponentSource implements SourceWithSlotsInterface {
23
24  /**
25   * {@inheritdoc}
26   */
27  public function getChoice(array $settings): string {
28    return $settings['component']['component_id'] ?? $settings['component_id'] ?? '';
29  }
30
31  /**
32   * {@inheritdoc}
33   */
34  public function getSlotDefinitions(): array {
35    $component_id = $this->settings['component']['component_id'] ?? '';
36
37    if (!$component_id) {
38      return [];
39    }
40
41    try {
42      $definition = $this->componentManager->getDefinition($component_id);
43    }
44    catch (\Throwable $th) {
45      return [];
46    }
47
48    return $definition['slots'] ?? [];
49  }
50
51  /**
52   * {@inheritdoc}
53   */
54  public function getSlotValues(): array {
55    $slots = [];
56
57    foreach ($this->settings['component']['slots'] ?? [] as $slot_id => $slot) {
58      if (isset($slot['sources'])) {
59        $slots[$slot_id] = $this->getSlotValue($slot_id);
60      }
61    }
62
63    return $slots;
64  }
65
66  /**
67   * {@inheritdoc}
68   */
69  public function getSlotValue(string $slot_id): array {
70    return $this->settings['component']['slots'][$slot_id]['sources'] ?? [];
71  }
72
73  /**
74   * {@inheritdoc}
75   */
76  public function setSlotValue(string $slot_id, array $slot): array {
77    $this->settings['component']['slots'][$slot_id]['sources'] = $slot;
78
79    return $this->settings;
80  }
81
82  /**
83   * {@inheritdoc}
84   */
85  public function setSlotRenderable(array $build, string $slot_id, array $slot): array {
86    $build['#slots'][$slot_id] = $slot;
87    // Prevent the slot to be generated again.
88    unset($build['#ui_patterns']['slots'][$slot_id]);
89
90    return $build;
91  }
92
93  /**
94   * {@inheritdoc}
95   */
96  public static function getSlotPath(string $slot_id): array {
97    return ['component', 'slots', $slot_id, 'sources'];
98  }
99
100  /**
101   * {@inheritdoc}
102   */
103  public function settingsSummary(): array {
104    $data = $this->getSetting('component');
105
106    if (empty($data) || !isset($data['component_id'])) {
107      return [];
108    }
109
110    if (empty($data['props']) && empty($data['variant_id'])) {
111      return [];
112    }
113
114    try {
115      $component = $this->componentManager->getDefinition($data['component_id']);
116    }
117    catch (\Throwable $th) {
118      return [];
119    }
120
121    $variant = $this->buildVariantSummary($component, $data);
122    $props = $this->buildPropsSummary($component, $data);
123
124    $items = \array_merge($variant, $props);
125
126    return $items;
127  }
128
129  /**
130   * {@inheritdoc}
131   */
132  public function settingsFormPropsOnly(array $form, FormStateInterface $form_state): array {
133    $form = $this->settingsForm($form, $form_state);
134    $data = $this->getSetting('component');
135    $component_id = $data['component_id'] ?? NULL;
136
137    if (!$component_id) {
138      return $form;
139    }
140
141    if (!isset($form['component']['component_id'])) {
142      $form['component']['component_id'] = [
143        '#type' => 'hidden',
144        '#value' => $component_id,
145      ];
146    }
147    $form['component']['#render_slots'] = FALSE;
148    $form['component']['#component_id'] = $component_id;
149
150    return \array_merge(
151      ['info' => $this->getComponentMetadata($component_id)],
152      $form
153    );
154  }
155
156  /**
157   * Get the group name for this source plugin.
158   *
159   * This method will be implemented in Drupal\ui_patterns\SourceInterface.
160   *
161   * @return string
162   *   The name of the group.
163   */
164  public function getGroup(): string {
165    $component_id = $this->settings['component']['component_id'] ?? '';
166
167    if (!$component_id) {
168      return '';
169    }
170
171    $definition = $this->componentManager->getDefinition($component_id);
172
173    return $definition['group'] ?? '';
174  }
175
176  /**
177   * Get component metadata.
178   *
179   * @param string $component_id
180   *   The component ID.
181   *
182   * @return array
183   *   A renderable array.
184   */
185  protected function getComponentMetadata(string $component_id): array {
186    $component = $this->componentManager->find($component_id);
187    $build = [];
188
189    if ($description = $component->metadata->description) {
190      $description = [
191        [
192          '#type' => 'html_tag',
193          '#tag' => 'p',
194          '#value' => $description,
195          '#attributes' => [
196            'class' => ['description'],
197          ],
198        ],
199        [
200          '#type' => 'html_tag',
201          '#tag' => 'sl-button',
202          '#value' => new TranslatableMarkup('Hide description'),
203          '#attributes' => [
204            'size' => 'small',
205            'variant' => 'default',
206            'class' => ['db-description-toggle'],
207          ],
208        ],
209      ];
210      $build[] = [
211        '#type' => 'html_tag',
212        '#tag' => 'div',
213        'content' => $description,
214      ];
215    }
216
217    return $build;
218  }
219
220  /**
221   * Builds variant summary items.
222   *
223   * @param array $component
224   *   The component definition.
225   * @param array $data
226   *   The component data.
227   *
228   * @return array
229   *   Summary items for variants.
230   */
231  private function buildVariantSummary(array $component, array $data): array {
232    if (!isset($data['variant_id']['source']['value'])
233        || $data['variant_id']['source']['value'] === 'default') {
234      return [];
235    }
236
237    $variantValue = $data['variant_id']['source']['value'];
238    $variantLabel = $component['variants'][$variantValue]['title'] ?? $variantValue;
239
240    if (empty($variantLabel)) {
241      return [];
242    }
243
244    return [new TranslatableMarkup('Variant: @variant', ['@variant' => $variantLabel])];
245  }
246
247  /**
248   * Builds props summary items.
249   *
250   * @param array $component
251   *   The component definition.
252   * @param array $data
253   *   The component data.
254   *
255   * @return array
256   *   Summary items for props.
257   */
258  private function buildPropsSummary(array $component, array $data): array {
259    if (empty($data['props'])) {
260      return [];
261    }
262
263    $items = [];
264    $propertyConfigs = $component['props']['properties'] ?? [];
265
266    foreach ($data['props'] as $sourceId => $sourceConfig) {
267      $summaryItem = $this->processProperty(
268        $sourceConfig,
269        $propertyConfigs[$sourceId] ?? NULL,
270        $sourceId
271      );
272
273      if ($summaryItem) {
274        $items[] = $summaryItem;
275      }
276    }
277
278    return $items;
279  }
280
281  /**
282   * Processes a property configuration to generate a summary string.
283   *
284   * @param array $sourceConfig
285   *   The source configuration array.
286   * @param array|null $propertyConfig
287   *   The property configuration array or NULL if not available.
288   * @param string $sourceId
289   *   The source identifier.
290   *
291   * @return string|null
292   *   The formatted summary string or NULL if no value is available.
293   */
294  private function processProperty(array $sourceConfig, ?array $propertyConfig, string $sourceId): ?string {
295    if ($this->isUiStyleAttribute($sourceConfig)) {
296      return $this->formatUiStyleSummary($sourceConfig, $propertyConfig);
297    }
298
299    return $this->processStandardProperty($sourceConfig, $propertyConfig, $sourceId);
300  }
301
302  /**
303   * Checks if the source configuration is for UI style attributes.
304   *
305   * @param array $sourceConfig
306   *   The source configuration array.
307   *
308   * @return bool
309   *   TRUE if it's a UI style attribute configuration, FALSE otherwise.
310   */
311  private function isUiStyleAttribute(array $sourceConfig): bool {
312    return ($sourceConfig['source_id'] ?? NULL) === 'ui_styles_attributes'
313          && isset($sourceConfig['source']['styles']['selected']);
314  }
315
316  /**
317   * Formats a UI style summary string.
318   *
319   * @param array $sourceConfig
320   *   The source configuration array.
321   * @param array|null $propertyConfig
322   *   The property configuration array or NULL if not available.
323   *
324   * @return string|null
325   *   The formatted style summary or NULL if no styles are selected.
326   */
327  private function formatUiStyleSummary(array $sourceConfig, ?array $propertyConfig): ?string {
328    $selectedStyles = $sourceConfig['source']['styles']['selected'];
329
330    if (empty($selectedStyles)) {
331      return NULL;
332    }
333
334    $mainLabel = $propertyConfig['title'] ?? '';
335    $firstStyle = \array_key_first($selectedStyles);
336
337    return $firstStyle ? \sprintf('%s - %s', $mainLabel, $firstStyle) : NULL;
338  }
339
340  /**
341   * Processes a standard property configuration to generate a summary string.
342   *
343   * @param array $sourceConfig
344   *   The source configuration array.
345   * @param array|null $propertyConfig
346   *   The property configuration array or NULL if not available.
347   * @param string $sourceId
348   *   The source identifier.
349   *
350   * @return string|null
351   *   The formatted summary string or NULL if no value is available.
352   */
353  private function processStandardProperty(array $sourceConfig, ?array $propertyConfig, string $sourceId): ?string {
354    if (!isset($sourceConfig['source']['value'])) {
355      return NULL;
356    }
357
358    $value = $sourceConfig['source']['value'];
359    $processedValue = self::normalizeValue($value);
360
361    if ($processedValue === NULL) {
362      return NULL;
363    }
364
365    $label = $propertyConfig['title'] ?? $sourceId;
366
367    return \sprintf('%s: %s', $label, $processedValue);
368  }
369
370  /**
371   * Normalizes a value to a string representation.
372   *
373   * @param mixed $value
374   *   The value to normalize (array or string).
375   *
376   * @return string|null
377   *   The normalized string value or NULL if empty/invalid.
378   */
379  private static function normalizeValue($value): ?string {
380    if (\is_array($value)) {
381      $str = self::flattenArrayToString($value);
382
383      return $str !== '' ? $str : NULL;
384    }
385
386    return \is_string($value) && $value !== '' ? $value : NULL;
387  }
388
389  /**
390   * Utility to stringify a nested array.
391   *
392   * @param array $array
393   *   The $array to normalize (array or string).
394   *
395   * @return string
396   *   The flatten string.
397   */
398  private static function flattenArrayToString(array $array): string {
399    $result = [];
400
401    foreach ($array as $key => $value) {
402      if (\is_array($value)) {
403        if (\is_int($key)) {
404          $result[] = self::flattenArrayToString($value);
405        }
406        else {
407          $result[] = $key . ': {' . self::flattenArrayToString($value) . '}';
408        }
409      }
410      elseif (!empty($value)) {
411        if (\is_int($key)) {
412          $result[] = (string) $value;
413        }
414        else {
415          $result[] = "{$key}{$value}";
416        }
417      }
418    }
419
420    return \implode(', ', $result);
421  }
422
423}