Code Coverage
 
Lines
Branches
Paths
Functions and Methods
Classes and Traits
Total
58.11% covered (warning)
58.11%
43 / 74
61.33% covered (warning)
61.33%
46 / 75
12.87% covered (danger)
12.87%
13 / 101
54.55% covered (warning)
54.55%
6 / 11
CRAP
0.00% covered (danger)
0.00%
0 / 1
PatternPreset
58.11% covered (warning)
58.11%
43 / 74
61.33% covered (warning)
61.33%
46 / 75
12.87% covered (danger)
12.87%
13 / 101
54.55% covered (warning)
54.55%
6 / 11
1098.29
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
18.18% covered (danger)
18.18%
2 / 11
44.44% covered (danger)
44.44%
4 / 9
16.67% covered (danger)
16.67%
1 / 6
0.00% covered (danger)
0.00%
0 / 1
19.47
 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\Component\Plugin\Exception\PluginException;
8use Drupal\Core\Config\Entity\ConfigEntityBase;
9use Drupal\Core\Entity\Attribute\ConfigEntityType;
10use Drupal\Core\Entity\EntityDeleteForm;
11use Drupal\Core\StringTranslation\TranslatableMarkup;
12use Drupal\display_builder\Form\PatternPresetForm;
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 a missing or malformed plugin (e.g. after config import),
180    // return empty rather than crashing. Unexpected exceptions are logged.
181    try {
182      return $this->getContextFromSource($this->sources['source_id'], $this->sources['source']);
183    }
184    catch (PluginException) {
185      // Plugin no longer exists or config is malformed â€” silently skip.
186      return [];
187    }
188    catch (\Exception $e) {
189      // Unexpected runtime error from plugin code: log and degrade gracefully.
190      \Drupal::logger('display_builder')->warning(
191        'PatternPreset @id: unexpected exception resolving contexts: @message',
192        ['@id' => $this->id(), '@message' => $e->getMessage()],
193      );
194
195      return [];
196    }
197  }
198
199  /**
200   * {@inheritdoc}
201   */
202  public function calculateDependencies(): self {
203    parent::calculateDependencies();
204
205    // The root level is a single nestable source plugin.
206    if (!isset($this->sources['source_id'])) {
207      return $this;
208    }
209    // This will automatically be done by parent::calculateDependencies() if we
210    // implement EntityWithPluginCollectionInterface.
211    $configuration = [
212      'settings' => $this->sources['source'] ?? [],
213    ];
214    /** @var \Drupal\ui_patterns\SourceInterface $source */
215    $source = $this->sourcePluginManager()->createInstance($this->sources['source_id'], $configuration);
216    $this->addDependencies($source->calculateDependencies());
217
218    return $this;
219  }
220
221  /**
222   * {@inheritdoc}
223   */
224  public function areContextsSatisfied(array $contexts): bool {
225    $context_definitions = $this->getContexts();
226
227    if (empty($context_definitions)) {
228      return TRUE;
229    }
230
231    foreach ($context_definitions as $key => $context_definition) {
232      if (!$context_definition->isRequired()) {
233        continue;
234      }
235
236      if (!\array_key_exists($key, $contexts)) {
237        return FALSE;
238      }
239
240      if (!$context_definition->isSatisfiedBy($contexts[$key])) {
241        return FALSE;
242      }
243    }
244
245    return TRUE;
246  }
247
248  /**
249   * Recursively get the source contexts.
250   *
251   * @param string $source_id
252   *   Source plugin ID.
253   * @param array $source
254   *   Source plugin configuration.
255   *
256   * @return array
257   *   Context definitions of the source.
258   */
259  private function getContextFromSource(string $source_id, array $source): array {
260    /** @var \Drupal\ui_patterns\SourceInterface $source */
261    $source = $this->sourcePluginManager()->createInstance($source_id, ['settings' => $source]);
262
263    if ($source->getPluginId() === 'component') {
264      return $this->getContextsFromComponent($source);
265    }
266
267    // @todo Traverse also context switchers.
268    return $source->getContextDefinitions();
269  }
270
271  /**
272   * Get contexts from component.
273   *
274   * Go through all slots and props to get the nested sources contexts.
275   *
276   * @param \Drupal\ui_patterns\SourceInterface $source
277   *   Source plugin.
278   *
279   * @return array
280   *   Context definitions of the component source.
281   */
282  private function getContextsFromComponent(SourceInterface $source): array {
283    $contexts = [];
284    $slots = $source->getSetting('component')['slots'] ?? [];
285
286    foreach ($slots as $slot) {
287      foreach ($slot['sources'] ?? [] as $slot_source) {
288        $contexts = \array_merge($contexts, $this->getContextFromSource($slot_source['source_id'], $slot_source['source']));
289      }
290    }
291
292    $props = $source->getSetting('component')['props'] ?? [];
293
294    foreach ($props as $prop_source) {
295      $contexts = \array_merge($contexts, $this->getContextFromSource($prop_source['source_id'], $prop_source['source']));
296    }
297
298    return $contexts;
299  }
300
301  /**
302   * Recursively fill the node_id key.
303   *
304   * @param array $array
305   *   The array reference.
306   */
307  private static function fillInternalId(array &$array): void {
308    if (isset($array['source_id']) && !isset($array['node_id'])) {
309      $array['node_id'] = \uniqid();
310    }
311
312    foreach ($array as &$value) {
313      if (\is_array($value)) {
314        self::fillInternalId($value);
315      }
316    }
317  }
318
319  /**
320   * Gets the source plugin manager.
321   *
322   * @return \Drupal\ui_patterns\SourcePluginManager
323   *   The source plugin manager.
324   */
325  private function sourcePluginManager(): SourcePluginManager {
326    return $this->sourcePluginManager ??= \Drupal::service('plugin.manager.ui_patterns_source');
327  }
328
329  /**
330   * Slot source proxy.
331   *
332   * @return \Drupal\display_builder\SlotSourceProxy
333   *   The slot source proxy.
334   */
335  private function slotSourceProxy(): SlotSourceProxy {
336    return $this->slotSourceProxy ??= \Drupal::service('display_builder.slot_sources_proxy');
337  }
338
339}

Branches

Below are the source code lines that represent each code branch as identified by Xdebug. Please note a branch is not necessarily coterminous with a line, a line may contain multiple branches and therefore show up more than once. Please also be aware that some branches may be implicit rather than explicit, e.g. an if statement always has an else as part of its logical flow even if you didn't write one.

PatternPreset->areContextsSatisfied
224  public function areContextsSatisfied(array $contexts): bool {
225    $context_definitions = $this->getContexts();
226
227    if (empty($context_definitions)) {
228      return TRUE;
231    foreach ($context_definitions as $key => $context_definition) {
231    foreach ($context_definitions as $key => $context_definition) {
231    foreach ($context_definitions as $key => $context_definition) {
232      if (!$context_definition->isRequired()) {
233        continue;
236      if (!\array_key_exists($key, $contexts)) {
237        return FALSE;
240      if (!$context_definition->isSatisfiedBy($contexts[$key])) {
241        return FALSE;
231    foreach ($context_definitions as $key => $context_definition) {
231    foreach ($context_definitions as $key => $context_definition) {
232      if (!$context_definition->isRequired()) {
233        continue;
234      }
235
236      if (!\array_key_exists($key, $contexts)) {
237        return FALSE;
238      }
239
240      if (!$context_definition->isSatisfiedBy($contexts[$key])) {
241        return FALSE;
242      }
243    }
244
245    return TRUE;
246  }
PatternPreset->calculateDependencies
203    parent::calculateDependencies();
204
205    // The root level is a single nestable source plugin.
206    if (!isset($this->sources['source_id'])) {
207      return $this;
212      'settings' => $this->sources['source'] ?? [],
213    ];
214    /** @var \Drupal\ui_patterns\SourceInterface $source */
215    $source = $this->sourcePluginManager()->createInstance($this->sources['source_id'], $configuration);
216    $this->addDependencies($source->calculateDependencies());
217
218    return $this;
219  }
PatternPreset->fillInternalId
307  private static function fillInternalId(array &$array): void {
308    if (isset($array['source_id']) && !isset($array['node_id'])) {
308    if (isset($array['source_id']) && !isset($array['node_id'])) {
308    if (isset($array['source_id']) && !isset($array['node_id'])) {
309      $array['node_id'] = \uniqid();
310    }
311
312    foreach ($array as &$value) {
312    foreach ($array as &$value) {
312    foreach ($array as &$value) {
313      if (\is_array($value)) {
312    foreach ($array as &$value) {
313      if (\is_array($value)) {
314        self::fillInternalId($value);
312    foreach ($array as &$value) {
312    foreach ($array as &$value) {
313      if (\is_array($value)) {
314        self::fillInternalId($value);
315      }
316    }
317  }
PatternPreset->getContextFromSource
259  private function getContextFromSource(string $source_id, array $source): array {
260    /** @var \Drupal\ui_patterns\SourceInterface $source */
261    $source = $this->sourcePluginManager()->createInstance($source_id, ['settings' => $source]);
262
263    if ($source->getPluginId() === 'component') {
264      return $this->getContextsFromComponent($source);
268    return $source->getContextDefinitions();
269  }
PatternPreset->getContexts
175    if (!isset($this->sources['source_id']) || !isset($this->sources['source'])) {
175    if (!isset($this->sources['source_id']) || !isset($this->sources['source'])) {
175    if (!isset($this->sources['source_id']) || !isset($this->sources['source'])) {
176      return [];
181    try {
182      return $this->getContextFromSource($this->sources['source_id'], $this->sources['source']);
184    catch (PluginException) {
186      return [];
188    catch (\Exception $e) {
190      \Drupal::logger('display_builder')->warning(
191        'PatternPreset @id: unexpected exception resolving contexts: @message',
192        ['@id' => $this->id(), '@message' => $e->getMessage()],
193      );
194
195      return [];
196    }
197  }
PatternPreset->getContextsFromComponent
282  private function getContextsFromComponent(SourceInterface $source): array {
283    $contexts = [];
284    $slots = $source->getSetting('component')['slots'] ?? [];
285
286    foreach ($slots as $slot) {
286    foreach ($slots as $slot) {
287      foreach ($slot['sources'] ?? [] as $slot_source) {
287      foreach ($slot['sources'] ?? [] as $slot_source) {
287      foreach ($slot['sources'] ?? [] as $slot_source) {
288        $contexts = \array_merge($contexts, $this->getContextFromSource($slot_source['source_id'], $slot_source['source']));
286    foreach ($slots as $slot) {
287      foreach ($slot['sources'] ?? [] as $slot_source) {
286    foreach ($slots as $slot) {
287      foreach ($slot['sources'] ?? [] as $slot_source) {
288        $contexts = \array_merge($contexts, $this->getContextFromSource($slot_source['source_id'], $slot_source['source']));
289      }
290    }
291
292    $props = $source->getSetting('component')['props'] ?? [];
293
294    foreach ($props as $prop_source) {
294    foreach ($props as $prop_source) {
294    foreach ($props as $prop_source) {
295      $contexts = \array_merge($contexts, $this->getContextFromSource($prop_source['source_id'], $prop_source['source']));
294    foreach ($props as $prop_source) {
295      $contexts = \array_merge($contexts, $this->getContextFromSource($prop_source['source_id'], $prop_source['source']));
296    }
297
298    return $contexts;
299  }
PatternPreset->getGroup
112    if (isset($this->group) && !empty($this->group)) {
112    if (isset($this->group) && !empty($this->group)) {
112    if (isset($this->group) && !empty($this->group)) {
113      return $this->group;
116    if (isset($this->sources['source'], $this->sources['source_id'])) {
116    if (isset($this->sources['source'], $this->sources['source_id'])) {
116    if (isset($this->sources['source'], $this->sources['source_id'])) {
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();
130      $this->save();
131    }
132
133    return !empty($this->group) ? $this->group : NULL;
133    return !empty($this->group) ? $this->group : NULL;
133    return !empty($this->group) ? $this->group : NULL;
133    return !empty($this->group) ? $this->group : NULL;
133    return !empty($this->group) ? $this->group : NULL;
134  }
PatternPreset->getSources
150  public function getSources(array $contexts = [], bool $fillInternalId = TRUE): array {
151    $data = $this->get('sources') ?? [];
152
153    if (isset($data[0]) && \count($data) === 1) {
153    if (isset($data[0]) && \count($data) === 1) {
153    if (isset($data[0]) && \count($data) === 1) {
154      $data = \reset($data);
155    }
156
157    if (empty($data) || !isset($data['source_id'])) {
157    if (empty($data) || !isset($data['source_id'])) {
157    if (empty($data) || !isset($data['source_id'])) {
157    if (empty($data) || !isset($data['source_id'])) {
158      return [];
161    if ($fillInternalId) {
162      self::fillInternalId($data);
163    }
164
165    return $data;
165    return $data;
166  }
PatternPreset->getSummary
140    $contexts = [];
141    $data = $this->getSources($contexts, FALSE);
142    $data = $this->slotSourceProxy()->getLabelWithSummary($data);
143
144    return $data['summary'] ?: $data['label'];
145  }
PatternPreset->slotSourceProxy
336    return $this->slotSourceProxy ??= \Drupal::service('display_builder.slot_sources_proxy');
337  }
PatternPreset->sourcePluginManager
326    return $this->sourcePluginManager ??= \Drupal::service('plugin.manager.ui_patterns_source');
327  }