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}

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
215  public function areContextsSatisfied(array $contexts): bool {
216    $context_definitions = $this->getContexts();
217
218    if (empty($context_definitions)) {
219      return TRUE;
222    foreach ($context_definitions as $key => $context_definition) {
222    foreach ($context_definitions as $key => $context_definition) {
222    foreach ($context_definitions as $key => $context_definition) {
223      if (!$context_definition->isRequired()) {
224        continue;
227      if (!\array_key_exists($key, $contexts)) {
228        return FALSE;
231      if (!$context_definition->isSatisfiedBy($contexts[$key])) {
232        return FALSE;
222    foreach ($context_definitions as $key => $context_definition) {
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  }
PatternPreset->calculateDependencies
194    parent::calculateDependencies();
195
196    // The root level is a single nestable source plugin.
197    if (!isset($this->sources['source_id'])) {
198      return $this;
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  }
PatternPreset->fillInternalId
298  private static function fillInternalId(array &$array): void {
299    if (isset($array['source_id']) && !isset($array['node_id'])) {
299    if (isset($array['source_id']) && !isset($array['node_id'])) {
299    if (isset($array['source_id']) && !isset($array['node_id'])) {
300      $array['node_id'] = \uniqid();
301    }
302
303    foreach ($array as &$value) {
303    foreach ($array as &$value) {
303    foreach ($array as &$value) {
304      if (\is_array($value)) {
303    foreach ($array as &$value) {
304      if (\is_array($value)) {
305        self::fillInternalId($value);
303    foreach ($array as &$value) {
303    foreach ($array as &$value) {
304      if (\is_array($value)) {
305        self::fillInternalId($value);
306      }
307    }
308  }
PatternPreset->getContextFromSource
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);
259    return $source->getContextDefinitions();
260  }
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 [];
180    try {
181      $contexts = $this->getContextFromSource($this->sources['source_id'], $this->sources['source']);
182
183      return $contexts;
185    catch (\Throwable $th) {
186      return [];
187    }
188  }
PatternPreset->getContextsFromComponent
273  private function getContextsFromComponent(SourceInterface $source): array {
274    $contexts = [];
275    $slots = $source->getSetting('component')['slots'] ?? [];
276
277    foreach ($slots as $slot) {
277    foreach ($slots as $slot) {
278      foreach ($slot['sources'] ?? [] as $slot_source) {
278      foreach ($slot['sources'] ?? [] as $slot_source) {
278      foreach ($slot['sources'] ?? [] as $slot_source) {
279        $contexts = \array_merge($contexts, $this->getContextFromSource($slot_source['source_id'], $slot_source['source']));
277    foreach ($slots as $slot) {
278      foreach ($slot['sources'] ?? [] as $slot_source) {
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) {
285    foreach ($props as $prop_source) {
285    foreach ($props as $prop_source) {
286      $contexts = \array_merge($contexts, $this->getContextFromSource($prop_source['source_id'], $prop_source['source']));
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  }
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
327    return $this->slotSourceProxy ??= \Drupal::service('display_builder.slot_sources_proxy');
328  }
PatternPreset->sourcePluginManager
317    return $this->sourcePluginManager ??= \Drupal::service('plugin.manager.ui_patterns_source');
318  }