Code Coverage
 
Lines
Branches
Paths
Functions and Methods
Classes and Traits
Total
63.77% covered (warning)
63.77%
44 / 69
63.01% covered (warning)
63.01%
46 / 73
13.00% covered (danger)
13.00%
13 / 100
54.55% covered (warning)
54.55%
6 / 11
CRAP
0.00% covered (danger)
0.00%
0 / 1
PatternPreset
63.77% covered (warning)
63.77%
44 / 69
63.01% covered (warning)
63.01%
46 / 73
13.00% covered (danger)
13.00%
13 / 100
54.55% covered (warning)
54.55%
6 / 11
1040.58
0.00% covered (danger)
0.00%
0 / 1
 getGroup
100.00% covered (success)
100.00%
11 / 11
100.00% covered (success)
100.00%
14 / 14
11.54% covered (danger)
11.54%
3 / 26
100.00% covered (success)
100.00%
1 / 1
30.92
 getSummary
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
2
 getSources
87.50% covered (warning)
87.50%
7 / 8
90.91% covered (success)
90.91%
10 / 11
8.33% covered (danger)
8.33%
2 / 24
0.00% covered (danger)
0.00%
0 / 1
33.73
 getContexts
50.00% covered (danger)
50.00%
3 / 6
57.14% covered (warning)
57.14%
4 / 7
20.00% covered (danger)
20.00%
1 / 5
0.00% covered (danger)
0.00%
0 / 1
12.19
 calculateDependencies
100.00% covered (success)
100.00%
9 / 9
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
 areContextsSatisfied
0.00% covered (danger)
0.00%
0 / 11
0.00% covered (danger)
0.00%
0 / 12
0.00% covered (danger)
0.00%
0 / 7
0.00% covered (danger)
0.00%
0 / 1
42
 getContextFromSource
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
 getContextsFromComponent
0.00% covered (danger)
0.00%
0 / 9
0.00% covered (danger)
0.00%
0 / 10
0.00% covered (danger)
0.00%
0 / 15
0.00% covered (danger)
0.00%
0 / 1
20
 fillInternalId
100.00% covered (success)
100.00%
5 / 5
100.00% covered (success)
100.00%
10 / 10
6.25% covered (danger)
6.25%
1 / 16
100.00% covered (success)
100.00%
1 / 1
25.60
 sourcePluginManager
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
 slotSourceProxy
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
1<?php
2
3declare(strict_types=1);
4
5namespace Drupal\display_builder\Entity;
6
7use Drupal\Core\Config\Entity\ConfigEntityBase;
8use Drupal\Core\Entity\Attribute\ConfigEntityType;
9use Drupal\Core\Entity\EntityDeleteForm;
10use Drupal\Core\StringTranslation\TranslatableMarkup;
11use Drupal\display_builder\Form\PatternPresetForm;
12use Drupal\display_builder\PatternPresetInterface;
13use Drupal\display_builder\SlotSourceProxy;
14use Drupal\display_builder_ui\PatternPresetListBuilder;
15use Drupal\ui_patterns\SourceInterface;
16use Drupal\ui_patterns\SourcePluginManager;
17
18/**
19 * Defines the Pattern preset entity type.
20 */
21#[ConfigEntityType(
22  id: 'pattern_preset',
23  label: new TranslatableMarkup('Pattern preset'),
24  label_collection: new TranslatableMarkup('Pattern presets'),
25  label_singular: new TranslatableMarkup('Pattern preset'),
26  label_plural: new TranslatableMarkup('Pattern presets'),
27  entity_keys: [
28    'id' => 'id',
29    'label' => 'label',
30    'theme' => 'theme',
31    'description' => 'description',
32    'group' => 'group',
33    'sources' => 'sources',
34    'weight' => 'weight',
35  ],
36  handlers: [
37    'list_builder' => PatternPresetListBuilder::class,
38    'form' => [
39      'add' => PatternPresetForm::class,
40      'edit' => PatternPresetForm::class,
41      'delete' => EntityDeleteForm::class,
42    ],
43  ],
44  links: [
45    'add-form' => '/admin/structure/display-builder/preset/add',
46    'delete-form' => '/admin/structure/display-builder/preset/{pattern_preset}/delete',
47    'collection' => '/admin/structure/display-builder/preset',
48  ],
49  admin_permission: 'administer Pattern preset',
50  constraints: [
51    'ImmutableProperties' => [
52      'id',
53    ],
54  ],
55  config_export: [
56    'id',
57    'label',
58    'description',
59    'group',
60    'sources',
61    'weight',
62  ],
63)]
64final class PatternPreset extends ConfigEntityBase implements PatternPresetInterface {
65
66  /**
67   * The preset ID.
68   */
69  protected string $id;
70
71  /**
72   * The preset label.
73   */
74  protected string $label = '';
75
76  /**
77   * The preset description.
78   */
79  protected string $description = '';
80
81  /**
82   * The preset group.
83   */
84  protected ?string $group = NULL;
85
86  /**
87   * The preset sources.
88   */
89  protected array $sources = [];
90
91  /**
92   * Weight to order the entity in lists.
93   *
94   * @var int
95   */
96  protected $weight = 0;
97
98  /**
99   * The UI Patterns source plugin manager.
100   */
101  protected SourcePluginManager $sourcePluginManager;
102
103  /**
104   * Slot source proxy.
105   */
106  protected SlotSourceProxy $slotSourceProxy;
107
108  /**
109   * {@inheritdoc}
110   */
111  public function getGroup(): ?string {
112    if (isset($this->group) && !empty($this->group)) {
113      return $this->group;
114    }
115
116    if (isset($this->sources['source'], $this->sources['source_id'])) {
117      $configuration = [
118        'settings' => $this->sources['source'] ?? [],
119      ];
120      /** @var \Drupal\ui_patterns\SourceInterface $source */
121      $source = $this->sourcePluginManager()->createInstance($this->sources['source_id'], $configuration);
122
123      // We check if the UI Patterns source plugin has a getGroup() method.
124      // At the moment, this method is not part of SourceInterface but is
125      // anticipated for a future update to the UI Patterns API.
126      // Using method_exists() ensures compatibility in the meantime.
127      if (\method_exists($source, 'getGroup')) {
128        $this->group = (string) $source->getGroup();
129      }
130      $this->save();
131    }
132
133    return !empty($this->group) ? $this->group : NULL;
134  }
135
136  /**
137   * {@inheritdoc}
138   */
139  public function getSummary(): string {
140    $contexts = [];
141    $data = $this->getSources($contexts, FALSE);
142    $data = $this->slotSourceProxy()->getLabelWithSummary($data);
143
144    return $data['summary'] ?: $data['label'];
145  }
146
147  /**
148   * {@inheritdoc}
149   */
150  public function getSources(array $contexts = [], bool $fillInternalId = TRUE): array {
151    $data = $this->get('sources') ?? [];
152
153    if (isset($data[0]) && \count($data) === 1) {
154      $data = \reset($data);
155    }
156
157    if (empty($data) || !isset($data['source_id'])) {
158      return [];
159    }
160
161    if ($fillInternalId) {
162      self::fillInternalId($data);
163    }
164
165    return $data;
166  }
167
168  /**
169   * {@inheritdoc}
170   *
171   * @see \Drupal\Core\Config\Entity\ConfigEntityInterface
172   */
173  public function getContexts(): array {
174    // The root level is a single nestable source plugin.
175    if (!isset($this->sources['source_id']) || !isset($this->sources['source'])) {
176      return [];
177    }
178
179    // In case of malformed json we don't want to fail, just ignore.
180    try {
181      $contexts = $this->getContextFromSource($this->sources['source_id'], $this->sources['source']);
182
183      return $contexts;
184    }
185    catch (\Throwable $th) {
186      return [];
187    }
188  }
189
190  /**
191   * {@inheritdoc}
192   */
193  public function calculateDependencies(): self {
194    parent::calculateDependencies();
195
196    // The root level is a single nestable source plugin.
197    if (!isset($this->sources['source_id'])) {
198      return $this;
199    }
200    // This will automatically be done by parent::calculateDependencies() if we
201    // implement EntityWithPluginCollectionInterface.
202    $configuration = [
203      'settings' => $this->sources['source'] ?? [],
204    ];
205    /** @var \Drupal\ui_patterns\SourceInterface $source */
206    $source = $this->sourcePluginManager()->createInstance($this->sources['source_id'], $configuration);
207    $this->addDependencies($source->calculateDependencies());
208
209    return $this;
210  }
211
212  /**
213   * {@inheritdoc}
214   */
215  public function areContextsSatisfied(array $contexts): bool {
216    $context_definitions = $this->getContexts();
217
218    if (empty($context_definitions)) {
219      return TRUE;
220    }
221
222    foreach ($context_definitions as $key => $context_definition) {
223      if (!$context_definition->isRequired()) {
224        continue;
225      }
226
227      if (!\array_key_exists($key, $contexts)) {
228        return FALSE;
229      }
230
231      if (!$context_definition->isSatisfiedBy($contexts[$key])) {
232        return FALSE;
233      }
234    }
235
236    return TRUE;
237  }
238
239  /**
240   * Recursively get the source contexts.
241   *
242   * @param string $source_id
243   *   Source plugin ID.
244   * @param array $source
245   *   Source plugin configuration.
246   *
247   * @return array
248   *   Context definitions of the source.
249   */
250  private function getContextFromSource(string $source_id, array $source): array {
251    /** @var \Drupal\ui_patterns\SourceInterface $source */
252    $source = $this->sourcePluginManager()->createInstance($source_id, ['settings' => $source]);
253
254    if ($source->getPluginId() === 'component') {
255      return $this->getContextsFromComponent($source);
256    }
257
258    // @todo Traverse also context switchers.
259    return $source->getContextDefinitions();
260  }
261
262  /**
263   * Get contexts from component.
264   *
265   * Go through all slots and props to get the nested sources contexts.
266   *
267   * @param \Drupal\ui_patterns\SourceInterface $source
268   *   Source plugin.
269   *
270   * @return array
271   *   Context definitions of the component source.
272   */
273  private function getContextsFromComponent(SourceInterface $source): array {
274    $contexts = [];
275    $slots = $source->getSetting('component')['slots'] ?? [];
276
277    foreach ($slots as $slot) {
278      foreach ($slot['sources'] ?? [] as $slot_source) {
279        $contexts = \array_merge($contexts, $this->getContextFromSource($slot_source['source_id'], $slot_source['source']));
280      }
281    }
282
283    $props = $source->getSetting('component')['props'] ?? [];
284
285    foreach ($props as $prop_source) {
286      $contexts = \array_merge($contexts, $this->getContextFromSource($prop_source['source_id'], $prop_source['source']));
287    }
288
289    return $contexts;
290  }
291
292  /**
293   * Recursively fill the node_id key.
294   *
295   * @param array $array
296   *   The array reference.
297   */
298  private static function fillInternalId(array &$array): void {
299    if (isset($array['source_id']) && !isset($array['node_id'])) {
300      $array['node_id'] = \uniqid();
301    }
302
303    foreach ($array as &$value) {
304      if (\is_array($value)) {
305        self::fillInternalId($value);
306      }
307    }
308  }
309
310  /**
311   * Gets the source plugin manager.
312   *
313   * @return \Drupal\ui_patterns\SourcePluginManager
314   *   The source plugin manager.
315   */
316  private function sourcePluginManager(): SourcePluginManager {
317    return $this->sourcePluginManager ??= \Drupal::service('plugin.manager.ui_patterns_source');
318  }
319
320  /**
321   * Slot source proxy.
322   *
323   * @return \Drupal\display_builder\SlotSourceProxy
324   *   The slot source proxy.
325   */
326  private function slotSourceProxy(): SlotSourceProxy {
327    return $this->slotSourceProxy ??= \Drupal::service('display_builder.slot_sources_proxy');
328  }
329
330}