Code Coverage
 
Lines
Branches
Paths
Functions and Methods
Classes and Traits
Total
58.93% covered (warning)
58.93%
33 / 56
53.85% covered (warning)
53.85%
28 / 52
19.30% covered (danger)
19.30%
11 / 57
54.55% covered (warning)
54.55%
6 / 11
CRAP
0.00% covered (danger)
0.00%
0 / 1
PatternPreset
58.93% covered (warning)
58.93%
33 / 56
53.85% covered (warning)
53.85%
28 / 52
19.30% covered (danger)
19.30%
11 / 57
54.55% covered (warning)
54.55%
6 / 11
605.37
0.00% covered (danger)
0.00%
0 / 1
 getGroup
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
 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
88.89% covered (warning)
88.89%
8 / 9
16.67% covered (danger)
16.67%
2 / 12
0.00% covered (danger)
0.00%
0 / 1
26.83
 getContexts
66.67% covered (warning)
66.67%
2 / 3
75.00% covered (warning)
75.00%
3 / 4
33.33% covered (danger)
33.33%
1 / 3
0.00% covered (danger)
0.00%
0 / 1
5.67
 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 / 11
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%
8 / 8
8.33% covered (danger)
8.33%
1 / 12
100.00% covered (success)
100.00%
1 / 1
24.26
 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 = '';
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    return $this->group;
113  }
114
115  /**
116   * {@inheritdoc}
117   */
118  public function getSummary(): string {
119    $contexts = [];
120    $data = $this->getSources($contexts, FALSE);
121    $data = $this->slotSourceProxy()->getLabelWithSummary($data);
122
123    return $data['summary'] ?: $data['label'];
124  }
125
126  /**
127   * {@inheritdoc}
128   */
129  public function getSources(array $contexts = [], bool $fillInternalId = TRUE): array {
130    $data = $this->get('sources') ?? [];
131
132    if (isset($data[0]) && \count($data) === 1) {
133      $data = \reset($data);
134    }
135
136    if (empty($data) || !isset($data['source_id'])) {
137      return [];
138    }
139
140    if ($fillInternalId) {
141      self::fillInternalId($data);
142    }
143
144    return $data;
145  }
146
147  /**
148   * {@inheritdoc}
149   *
150   * @see \Drupal\Core\Config\Entity\ConfigEntityInterface
151   */
152  public function getContexts(): array {
153    // The root level is a single nestable source plugin.
154    if (!isset($this->sources['source_id']) || !isset($this->sources['source'])) {
155      return [];
156    }
157
158    return $this->getContextFromSource($this->sources['source_id'], $this->sources['source']);
159  }
160
161  /**
162   * {@inheritdoc}
163   */
164  public function calculateDependencies(): self {
165    parent::calculateDependencies();
166
167    // The root level is a single nestable source plugin.
168    if (!isset($this->sources['source_id'])) {
169      return $this;
170    }
171    // This will automatically be done by parent::calculateDependencies() if we
172    // implement EntityWithPluginCollectionInterface.
173    $configuration = [
174      'settings' => $this->sources['source'] ?? [],
175    ];
176    /** @var \Drupal\ui_patterns\SourceInterface $source */
177    $source = $this->sourcePluginManager()->createInstance($this->sources['source_id'], $configuration);
178    $this->addDependencies($source->calculateDependencies());
179
180    return $this;
181  }
182
183  /**
184   * {@inheritdoc}
185   */
186  public function areContextsSatisfied(array $contexts): bool {
187    $context_definitions = $this->getContexts();
188
189    if (empty($context_definitions)) {
190      return TRUE;
191    }
192
193    foreach ($context_definitions as $key => $context_definition) {
194      if (!$context_definition->isRequired()) {
195        continue;
196      }
197
198      if (!\array_key_exists($key, $contexts)) {
199        return FALSE;
200      }
201
202      if (!$context_definition->isSatisfiedBy($contexts[$key])) {
203        return FALSE;
204      }
205    }
206
207    return TRUE;
208  }
209
210  /**
211   * Recursively get the source contexts.
212   *
213   * @param string $source_id
214   *   Source plugin ID.
215   * @param array $source
216   *   Source plugin configuration.
217   *
218   * @return array
219   *   Context definitions of the source.
220   */
221  private function getContextFromSource(string $source_id, array $source): array {
222    /** @var \Drupal\ui_patterns\SourceInterface $source */
223    $source = $this->sourcePluginManager()->createInstance($source_id, ['settings' => $source]);
224
225    if ($source->getPluginId() === 'component') {
226      return $this->getContextsFromComponent($source);
227    }
228
229    // @todo Traverse also context switchers.
230    return $source->getContextDefinitions();
231  }
232
233  /**
234   * Get contexts from component.
235   *
236   * Go through all slots and props to get the nested sources contexts.
237   *
238   * @param \Drupal\ui_patterns\SourceInterface $source
239   *   Source plugin.
240   *
241   * @return array
242   *   Context definitions of the component source.
243   */
244  private function getContextsFromComponent(SourceInterface $source): array {
245    $contexts = [];
246    $slots = $source->getSetting('component')['slots'] ?? [];
247
248    foreach ($slots as $slot) {
249      foreach ($slot['sources'] ?? [] as $slot_source) {
250        $contexts = \array_merge($contexts, $this->getContextFromSource($slot_source['source_id'], $slot_source['source']));
251      }
252    }
253
254    $props = $source->getSetting('component')['props'] ?? [];
255
256    foreach ($props as $prop_source) {
257      $contexts = \array_merge($contexts, $this->getContextFromSource($prop_source['source_id'], $prop_source['source']));
258    }
259
260    return $contexts;
261  }
262
263  /**
264   * Recursively fill the node_id key.
265   *
266   * @param array $array
267   *   The array reference.
268   */
269  private static function fillInternalId(array &$array): void {
270    if (isset($array['source_id']) && !isset($array['node_id'])) {
271      $array['node_id'] = \uniqid();
272    }
273
274    foreach ($array as &$value) {
275      if (\is_array($value)) {
276        self::fillInternalId($value);
277      }
278    }
279  }
280
281  /**
282   * Gets the source plugin manager.
283   *
284   * @return \Drupal\ui_patterns\SourcePluginManager
285   *   The source plugin manager.
286   */
287  private function sourcePluginManager(): SourcePluginManager {
288    return $this->sourcePluginManager ??= \Drupal::service('plugin.manager.ui_patterns_source');
289  }
290
291  /**
292   * Slot source proxy.
293   *
294   * @return \Drupal\display_builder\SlotSourceProxy
295   *   The slot source proxy.
296   */
297  private function slotSourceProxy(): SlotSourceProxy {
298    return $this->slotSourceProxy ??= \Drupal::service('display_builder.slot_sources_proxy');
299  }
300
301}