Code Coverage
 
Lines
Branches
Paths
Functions and Methods
Classes and Traits
Total
90.00% covered (success)
90.00%
171 / 190
88.98% covered (warning)
88.98%
113 / 127
53.33% covered (warning)
53.33%
48 / 90
74.07% covered (warning)
74.07%
20 / 27
CRAP
0.00% covered (danger)
0.00%
0 / 1
SourceTree
90.00% covered (success)
90.00%
171 / 190
88.98% covered (warning)
88.98%
113 / 127
53.33% covered (warning)
53.33%
48 / 90
74.07% covered (warning)
74.07%
20 / 27
583.31
0.00% covered (danger)
0.00%
0 / 1
 __construct
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
 rebuild
100.00% covered (success)
100.00%
5 / 5
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
 getTree
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
 getNormalizedStructure
0.00% covered (danger)
0.00%
0 / 5
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 getPathIndex
100.00% covered (success)
100.00%
4 / 4
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
 attachToRoot
100.00% covered (success)
100.00%
13 / 13
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
 attachToSlot
100.00% covered (success)
100.00%
17 / 17
100.00% covered (success)
100.00%
5 / 5
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
3
 moveToRoot
100.00% covered (success)
100.00%
7 / 7
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
 moveToSlot
100.00% covered (success)
100.00%
15 / 15
100.00% covered (success)
100.00%
11 / 11
100.00% covered (success)
100.00%
6 / 6
100.00% covered (success)
100.00%
1 / 1
6
 remove
100.00% covered (success)
100.00%
5 / 5
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
 hasNode
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
 getNodeData
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
 getNode
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
 getParentId
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
 setSource
100.00% covered (success)
100.00%
5 / 5
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
 setThirdPartySettings
100.00% covered (success)
100.00%
6 / 6
100.00% covered (success)
100.00%
5 / 5
66.67% covered (warning)
66.67%
2 / 3
100.00% covered (success)
100.00%
1 / 1
3.33
 generateNodeId
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
 normalize
100.00% covered (success)
100.00%
21 / 21
100.00% covered (success)
100.00%
12 / 12
25.00% covered (danger)
25.00%
3 / 12
100.00% covered (success)
100.00%
1 / 1
15.55
 denormalize
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
1
 injectChildren
100.00% covered (success)
100.00%
9 / 9
100.00% covered (success)
100.00%
10 / 10
22.22% covered (danger)
22.22%
2 / 9
100.00% covered (success)
100.00%
1 / 1
16.76
 buildPathIndex
87.50% covered (warning)
87.50%
14 / 16
85.71% covered (warning)
85.71%
12 / 14
33.33% covered (danger)
33.33%
3 / 9
0.00% covered (danger)
0.00%
0 / 1
16.67
 removeFromCurrentParent
80.00% covered (warning)
80.00%
16 / 20
73.33% covered (warning)
73.33%
11 / 15
27.27% covered (danger)
27.27%
3 / 11
0.00% covered (danger)
0.00%
0 / 1
32.62
 recursiveRemove
83.33% covered (warning)
83.33%
5 / 6
88.89% covered (warning)
88.89%
8 / 9
33.33% covered (danger)
33.33%
2 / 6
0.00% covered (danger)
0.00%
0 / 1
8.74
 isDescendant
100.00% covered (success)
100.00%
6 / 6
100.00% covered (success)
100.00%
6 / 6
33.33% covered (danger)
33.33%
1 / 3
100.00% covered (success)
100.00%
1 / 1
5.67
 getSourcePlugin
40.00% covered (danger)
40.00%
2 / 5
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
 getPluginClass
62.50% covered (warning)
62.50%
5 / 8
66.67% covered (warning)
66.67%
4 / 6
66.67% covered (warning)
66.67%
2 / 3
0.00% covered (danger)
0.00%
0 / 1
3.33
 getSourceManager
66.67% covered (warning)
66.67%
2 / 3
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
1<?php
2
3declare(strict_types=1);
4
5namespace Drupal\display_builder;
6
7use Drupal\Component\Plugin\PluginManagerInterface;
8use Drupal\Component\Utility\NestedArray;
9use Drupal\ui_patterns\SourceInterface;
10
11/**
12 * Manages hierarchical data with a high-performance normalized structure.
13 *
14 * This class converts nested source trees into a flat internal representation:
15 * - A 'nodes' map containing the raw configuration for each unique node.
16 * - A 'structure' map defining parent-child relationships and slot assignments.
17 *
18 * This normalization allows for O(1) or O(log N) operations when moving,
19 * updating, or retrieving specific nodes, regardless of tree depth. The
20 * tree is only denormalized back into a nested format when requested via
21 * ::getTree() for rendering or persistence.
22 */
23final class SourceTree {
24
25  /**
26   * Flat map of node data keyed by node_id.
27   */
28  private array $nodes = [];
29
30  /**
31   * Hierarchical structure of node IDs.
32   */
33  private array $structure = [];
34
35  /**
36   * List of root node IDs.
37   */
38  private array $root = [];
39
40  /**
41   * Cached path index.
42   */
43  private ?array $pathIndex = NULL;
44
45  /**
46   * The source plugin manager.
47   */
48  private ?PluginManagerInterface $sourceManager = NULL;
49
50  /**
51   * Cache of resolved plugin classes keyed by source_id.
52   */
53  private array $pluginClassCache = [];
54
55  /**
56   * Constructor.
57   *
58   * @param array $tree
59   *   Initial nested tree data.
60   * @param \Drupal\Component\Plugin\PluginManagerInterface|null $sourceManager
61   *   The source plugin manager.
62   */
63  public function __construct(array $tree = [], ?PluginManagerInterface $sourceManager = NULL) {
64    $this->sourceManager = $sourceManager;
65    $this->rebuild($tree);
66  }
67
68  /**
69   * Rebuild the internal normalized state from a nested tree.
70   *
71   * @param array $tree
72   *   The nested tree data.
73   */
74  public function rebuild(array $tree): void {
75    $this->nodes = [];
76    $this->structure = [];
77    $this->pluginClassCache = [];
78    $this->root = $this->normalize($tree, NULL, NULL);
79    $this->pathIndex = NULL;
80  }
81
82  /**
83   * Get the nested tree data.
84   *
85   * @return array
86   *   The full nested tree data.
87   */
88  public function getTree(): array {
89    return $this->denormalize($this->root);
90  }
91
92  /**
93   * Get the raw normalized structure (nodes, structure, root).
94   *
95   * Returns the internal flat representation before denormalization, useful
96   * for debugging and dev tooling.
97   *
98   * @return array
99   *   An array with keys 'nodes', 'structure', and 'root'.
100   */
101  public function getNormalizedStructure(): array {
102    return [
103      'nodes' => $this->nodes,
104      'structure' => $this->structure,
105      'root' => $this->root,
106    ];
107  }
108
109  /**
110   * Get the path index.
111   *
112   * @return array
113   *   The path index.
114   */
115  public function getPathIndex(): array {
116    if ($this->pathIndex === NULL) {
117      $this->pathIndex = [];
118      $this->buildPathIndex($this->root, [], $this->pathIndex);
119    }
120
121    return $this->pathIndex;
122  }
123
124  /**
125   * Attach a new source to root.
126   *
127   * @param int $position
128   *   The position in the root list.
129   * @param string $source_id
130   *   The source plugin ID.
131   * @param array $source_data
132   *   The source configuration data.
133   *
134   * @return string
135   *   The new node ID.
136   */
137  public function attachToRoot(int $position, string $source_id, array $source_data): string {
138    $node_id = $this->generateNodeId();
139    $this->nodes[$node_id] = [
140      'source_id' => $source_id,
141      'source' => $source_data,
142    ];
143    $this->structure[$node_id] = [
144      'parent' => NULL,
145      'slot' => NULL,
146      'slots' => [],
147    ];
148    \array_splice($this->root, $position, 0, [$node_id]);
149    $this->pathIndex = NULL;
150
151    return $node_id;
152  }
153
154  /**
155   * Attach a new source to a slot.
156   *
157   * @param string $parent_id
158   *   The parent node ID.
159   * @param string $slot_id
160   *   The slot ID.
161   * @param int $position
162   *   The position in the slot.
163   * @param string $source_id
164   *   The source plugin ID.
165   * @param array $source_data
166   *   The source configuration data.
167   *
168   * @return string|null
169   *   The new node ID or NULL if parent not found.
170   */
171  public function attachToSlot(string $parent_id, string $slot_id, int $position, string $source_id, array $source_data): ?string {
172    if (!isset($this->structure[$parent_id])) {
173      return NULL;
174    }
175
176    $node_id = $this->generateNodeId();
177    $this->nodes[$node_id] = [
178      'source_id' => $source_id,
179      'source' => $source_data,
180    ];
181    $this->structure[$node_id] = [
182      'parent' => $parent_id,
183      'slot' => $slot_id,
184      'slots' => [],
185    ];
186
187    if (!isset($this->structure[$parent_id]['slots'][$slot_id])) {
188      $this->structure[$parent_id]['slots'][$slot_id] = [];
189    }
190    \array_splice($this->structure[$parent_id]['slots'][$slot_id], $position, 0, [$node_id]);
191    $this->pathIndex = NULL;
192
193    return $node_id;
194  }
195
196  /**
197   * Move node to root.
198   *
199   * @param string $node_id
200   *   The node ID to move.
201   * @param int $position
202   *   The new position in the root list.
203   *
204   * @return bool
205   *   TRUE if success.
206   */
207  public function moveToRoot(string $node_id, int $position): bool {
208    if (!$this->removeFromCurrentParent($node_id)) {
209      return FALSE;
210    }
211
212    $this->structure[$node_id]['parent'] = NULL;
213    $this->structure[$node_id]['slot'] = NULL;
214    \array_splice($this->root, $position, 0, [$node_id]);
215    $this->pathIndex = NULL;
216
217    return TRUE;
218  }
219
220  /**
221   * Move node to a slot.
222   *
223   * @param string $node_id
224   *   The node ID to move.
225   * @param string $parent_id
226   *   The target parent ID.
227   * @param string $slot_id
228   *   The target slot ID.
229   * @param int $position
230   *   The position in the target slot.
231   *
232   * @return bool
233   *   TRUE if success.
234   */
235  public function moveToSlot(string $node_id, string $parent_id, string $slot_id, int $position): bool {
236    if (!isset($this->structure[$parent_id])) {
237      return FALSE;
238    }
239
240    // Forbidden move: moving a node into itself.
241    if ($node_id === $parent_id) {
242      return FALSE;
243    }
244
245    // Forbidden move: moving a parent into its own descendant.
246    if ($this->isDescendant($parent_id, $node_id)) {
247      return FALSE;
248    }
249
250    if (!$this->removeFromCurrentParent($node_id)) {
251      return FALSE;
252    }
253
254    $this->structure[$node_id]['parent'] = $parent_id;
255    $this->structure[$node_id]['slot'] = $slot_id;
256
257    if (!isset($this->structure[$parent_id]['slots'][$slot_id])) {
258      $this->structure[$parent_id]['slots'][$slot_id] = [];
259    }
260    \array_splice($this->structure[$parent_id]['slots'][$slot_id], $position, 0, [$node_id]);
261    $this->pathIndex = NULL;
262
263    return TRUE;
264  }
265
266  /**
267   * Remove a node and its descendants.
268   *
269   * @param string $node_id
270   *   The node ID to remove.
271   *
272   * @return bool
273   *   TRUE if success.
274   */
275  public function remove(string $node_id): bool {
276    if (!$this->removeFromCurrentParent($node_id)) {
277      return FALSE;
278    }
279    $this->recursiveRemove($node_id);
280    $this->pathIndex = NULL;
281
282    return TRUE;
283  }
284
285  /**
286   * Check if a node exists.
287   *
288   * @param string $node_id
289   *   The node ID.
290   *
291   * @return bool
292   *   TRUE if it exists.
293   */
294  public function hasNode(string $node_id): bool {
295    return isset($this->nodes[$node_id]);
296  }
297
298  /**
299   * Get flat node data (no children).
300   *
301   * @param string $node_id
302   *   The node ID.
303   *
304   * @return array|null
305   *   The flat node data or NULL.
306   */
307  public function getNodeData(string $node_id): ?array {
308    return $this->nodes[$node_id] ?? NULL;
309  }
310
311  /**
312   * Get a node data by ID (nested subtree).
313   *
314   * @param string $node_id
315   *   The node ID.
316   *
317   * @return array|null
318   *   The node data (nested structure for that node) or NULL.
319   */
320  public function getNode(string $node_id): ?array {
321    if (!isset($this->nodes[$node_id])) {
322      return NULL;
323    }
324
325    return $this->denormalize([$node_id])[0];
326  }
327
328  /**
329   * Get the parent ID of a node.
330   *
331   * @param string $node_id
332   *   The node ID.
333   *
334   * @return string|null
335   *   The parent ID or NULL if at root.
336   */
337  public function getParentId(string $node_id): ?string {
338    return $this->structure[$node_id]['parent'] ?? NULL;
339  }
340
341  /**
342   * Set source data for a node.
343   *
344   * @param string $node_id
345   *   The node ID.
346   * @param string $source_id
347   *   The source plugin ID.
348   * @param array $source_data
349   *   The source configuration data.
350   *
351   * @return bool
352   *   TRUE if success.
353   */
354  public function setSource(string $node_id, string $source_id, array $source_data): bool {
355    if (!isset($this->nodes[$node_id])) {
356      return FALSE;
357    }
358    $this->nodes[$node_id]['source_id'] = $source_id;
359    $this->nodes[$node_id]['source'] = $source_data;
360
361    return TRUE;
362  }
363
364  /**
365   * Set third party settings for a node.
366   *
367   * @param string $node_id
368   *   The node ID.
369   * @param string $island_id
370   *   The island (plugin) ID.
371   * @param array $data
372   *   The third party settings data.
373   *
374   * @return bool
375   *   TRUE if success.
376   */
377  public function setThirdPartySettings(string $node_id, string $island_id, array $data): bool {
378    if (!isset($this->nodes[$node_id])) {
379      return FALSE;
380    }
381
382    if (!isset($this->nodes[$node_id]['third_party_settings'])) {
383      $this->nodes[$node_id]['third_party_settings'] = [];
384    }
385    $this->nodes[$node_id]['third_party_settings'][$island_id] = $data;
386
387    return TRUE;
388  }
389
390  /**
391   * Generate a unique node ID.
392   *
393   * @return string
394   *   The generated node ID.
395   */
396  private function generateNodeId(): string {
397    return \bin2hex(\random_bytes(8));
398  }
399
400  /**
401   * Normalize a nested tree into flat maps.
402   *
403   * @param array $items
404   *   Nested items.
405   * @param string|null $parent_id
406   *   Current parent ID.
407   * @param string|null $slot_id
408   *   Current slot ID.
409   *
410   * @return array
411   *   List of node IDs at this level.
412   */
413  private function normalize(array $items, ?string $parent_id, ?string $slot_id): array {
414    $ids = [];
415
416    foreach ($items as $item) {
417      $node_id = $item['node_id'] ?? $this->generateNodeId();
418      $ids[] = $node_id;
419
420      $source_id = $item['source_id'] ?? '';
421      $plugin = $this->getSourcePlugin($source_id, $item['source'] ?? []);
422
423      $slots = [];
424
425      if ($plugin instanceof SourceWithSlotsInterface) {
426        foreach ($plugin->getSlotValues() as $child_slot_id => $data) {
427          $slots[$child_slot_id] = $this->normalize($data, $node_id, $child_slot_id);
428        }
429
430        foreach ($plugin->getSlotDefinitions() as $child_slot_id => $_) {
431          $path = $plugin::getSlotPath($child_slot_id);
432          NestedArray::unsetValue($item['source'], $path);
433        }
434      }
435
436      unset($item['node_id']);
437      $this->structure[$node_id] = [
438        'parent' => $parent_id,
439        'slot' => $slot_id,
440        'slots' => $slots,
441      ];
442      $this->nodes[$node_id] = $item;
443    }
444
445    return $ids;
446  }
447
448  /**
449   * Denormalize a list of node IDs into a nested tree.
450   *
451   * @param array $ids
452   *   The IDs to assemble.
453   *
454   * @return array
455   *   The nested tree.
456   */
457  private function denormalize(array $ids): array {
458    return \array_map(function ($id) {
459      $node = ['node_id' => $id] + $this->nodes[$id];
460
461      return $this->injectChildren($node, $this->structure[$id]['slots']);
462    }, $ids);
463  }
464
465  /**
466   * Inject children IDs back into a node as nested data.
467   *
468   * @param array $node
469   *   The node data.
470   * @param array $slots
471   *   The slots with children IDs.
472   *
473   * @return array
474   *   The node data with children injected.
475   */
476  private function injectChildren(array $node, array $slots): array {
477    if (empty($slots)) {
478      return $node;
479    }
480
481    $source_id = $node['source_id'] ?? '';
482    $class = $this->getPluginClass($source_id);
483
484    if ($class && \is_subclass_of($class, SourceWithSlotsInterface::class)) {
485      foreach ($slots as $slot_id => $child_ids) {
486        $path = $class::getSlotPath($slot_id);
487        NestedArray::setValue($node['source'], $path, $this->denormalize($child_ids));
488      }
489    }
490
491    return $node;
492  }
493
494  /**
495   * Build the path index recursively.
496   *
497   * @param array $ids
498   *   Current IDs level.
499   * @param array $current_path
500   *   Current path keys.
501   * @param array $index
502   *   The index to populate.
503   */
504  private function buildPathIndex(array $ids, array $current_path, array &$index): void {
505    foreach ($ids as $idx => $id) {
506      $path = [...$current_path, $idx];
507      $index[$id] = [
508        'path' => $path,
509        'parent' => $this->structure[$id]['parent'],
510      ];
511
512      $struct = $this->structure[$id];
513      $source_id = $this->nodes[$id]['source_id'];
514
515      if ($source_id === NULL) {
516        continue;
517      }
518      $class = $this->getPluginClass($source_id);
519
520      foreach ($struct['slots'] as $slot_id => $child_ids) {
521        if ($class && \is_subclass_of($class, SourceWithSlotsInterface::class)) {
522          $child_path = [...$path, 'source', ...$class::getSlotPath($slot_id)];
523        }
524        else {
525          continue;
526        }
527        $this->buildPathIndex($child_ids, $child_path, $index);
528      }
529    }
530  }
531
532  /**
533   * Remove a node from its parent's children list.
534   *
535   * @param string $node_id
536   *   The node ID.
537   *
538   * @return bool
539   *   TRUE if found and removed.
540   */
541  private function removeFromCurrentParent(string $node_id): bool {
542    if (!isset($this->structure[$node_id])) {
543      return FALSE;
544    }
545
546    $parent_id = $this->structure[$node_id]['parent'];
547    $slot_id = $this->structure[$node_id]['slot'];
548
549    if ($parent_id === NULL) {
550      $key = \array_search($node_id, $this->root, TRUE);
551
552      if ($key === FALSE) {
553        return FALSE;
554      }
555      \array_splice($this->root, (int) $key, 1);
556
557      return TRUE;
558    }
559
560    if ($slot_id === NULL || !isset($this->structure[$parent_id]['slots'][$slot_id])) {
561      return FALSE;
562    }
563
564    $child_ids = &$this->structure[$parent_id]['slots'][$slot_id];
565
566    if (!\is_array($child_ids)) {
567      return FALSE;
568    }
569    $key = \array_search($node_id, $child_ids, TRUE);
570
571    if ($key !== FALSE) {
572      \array_splice($child_ids, (int) $key, 1);
573
574      return TRUE;
575    }
576
577    return FALSE;
578  }
579
580  /**
581   * Recursively remove node and data.
582   *
583   * @param string $node_id
584   *   The node ID.
585   */
586  private function recursiveRemove(string $node_id): void {
587    if (!isset($this->structure[$node_id])) {
588      return;
589    }
590
591    foreach ($this->structure[$node_id]['slots'] as $child_ids) {
592      foreach ($child_ids as $child_id) {
593        $this->recursiveRemove($child_id);
594      }
595    }
596    unset($this->nodes[$node_id], $this->structure[$node_id]);
597  }
598
599  /**
600   * Check if a node is a descendant of another.
601   *
602   * @param string $node_id
603   *   The node ID to check.
604   * @param string $potential_ancestor_id
605   *   The potential ancestor ID.
606   *
607   * @return bool
608   *   TRUE if descendant.
609   */
610  private function isDescendant(string $node_id, string $potential_ancestor_id): bool {
611    $current_parent = $this->getParentId($node_id);
612
613    while ($current_parent !== NULL) {
614      if ($current_parent === $potential_ancestor_id) {
615        return TRUE;
616      }
617      $current_parent = $this->getParentId($current_parent);
618    }
619
620    return FALSE;
621  }
622
623  /**
624   * Get source plugin instance.
625   *
626   * @param string $source_id
627   *   The source plugin ID.
628   * @param array $source_configuration
629   *   The source configuration.
630   *
631   * @return \Drupal\ui_patterns\SourceInterface|null
632   *   The source plugin instance or NULL.
633   */
634  private function getSourcePlugin(string $source_id, array $source_configuration): ?SourceInterface {
635    try {
636      $plugin = $this->getSourceManager()->createInstance($source_id, ['settings' => $source_configuration]);
637
638      return $plugin instanceof SourceInterface ? $plugin : NULL;
639    }
640    catch (\Exception $e) {
641      // phpcs:ignore -- lazy-init required; see getSourceManager() docblock.
642      \Drupal::logger('display_builder')->warning('SourceTree: failed to instantiate source plugin %id: @message', ['%id' => $source_id, '@message' => $e->getMessage()]);
643
644      return NULL;
645    }
646  }
647
648  /**
649   * Get source plugin class.
650   *
651   * @param string $source_id
652   *   The source plugin ID.
653   *
654   * @return string|null
655   *   The plugin class or NULL.
656   */
657  private function getPluginClass(string $source_id): ?string {
658    if (\array_key_exists($source_id, $this->pluginClassCache)) {
659      return $this->pluginClassCache[$source_id];
660    }
661
662    try {
663      $definition = $this->getSourceManager()->getDefinition($source_id);
664      $this->pluginClassCache[$source_id] = $definition['class'] ?? NULL;
665    }
666    catch (\Exception $e) {
667      // phpcs:ignore -- lazy-init required; see getSourceManager() docblock.
668      \Drupal::logger('display_builder')->warning('SourceTree: failed to get definition for source plugin %id: @message', ['%id' => $source_id, '@message' => $e->getMessage()]);
669      $this->pluginClassCache[$source_id] = NULL;
670    }
671
672    return $this->pluginClassCache[$source_id];
673  }
674
675  /**
676   * Gets the UI Patterns source plugin manager.
677   *
678   * SourceTree is a plain value object instantiated as new SourceTree() from
679   * entity and plugin base classes where constructor injection is unavailable.
680   * The lazy-init fallback using \Drupal::service() is intentional and the
681   * only viable pattern for those call sites.
682   *
683   * @return \Drupal\Component\Plugin\PluginManagerInterface
684   *   The source plugin manager.
685   */
686  private function getSourceManager(): PluginManagerInterface {
687    if ($this->sourceManager === NULL) {
688      // phpcs:ignore -- lazy-init required; see method docblock.
689      $this->sourceManager = \Drupal::service('plugin.manager.ui_patterns_source');
690    }
691
692    return $this->sourceManager;
693  }
694
695}

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.

SourceTree->__construct
63  public function __construct(array $tree = [], ?PluginManagerInterface $sourceManager = NULL) {
64    $this->sourceManager = $sourceManager;
65    $this->rebuild($tree);
66  }
SourceTree->attachToRoot
137  public function attachToRoot(int $position, string $source_id, array $source_data): string {
138    $node_id = $this->generateNodeId();
139    $this->nodes[$node_id] = [
140      'source_id' => $source_id,
141      'source' => $source_data,
142    ];
143    $this->structure[$node_id] = [
144      'parent' => NULL,
145      'slot' => NULL,
146      'slots' => [],
147    ];
148    \array_splice($this->root, $position, 0, [$node_id]);
149    $this->pathIndex = NULL;
150
151    return $node_id;
152  }
SourceTree->attachToSlot
171  public function attachToSlot(string $parent_id, string $slot_id, int $position, string $source_id, array $source_data): ?string {
172    if (!isset($this->structure[$parent_id])) {
173      return NULL;
176    $node_id = $this->generateNodeId();
177    $this->nodes[$node_id] = [
178      'source_id' => $source_id,
179      'source' => $source_data,
180    ];
181    $this->structure[$node_id] = [
182      'parent' => $parent_id,
183      'slot' => $slot_id,
184      'slots' => [],
185    ];
186
187    if (!isset($this->structure[$parent_id]['slots'][$slot_id])) {
188      $this->structure[$parent_id]['slots'][$slot_id] = [];
189    }
190    \array_splice($this->structure[$parent_id]['slots'][$slot_id], $position, 0, [$node_id]);
190    \array_splice($this->structure[$parent_id]['slots'][$slot_id], $position, 0, [$node_id]);
191    $this->pathIndex = NULL;
192
193    return $node_id;
194  }
SourceTree->buildPathIndex
504  private function buildPathIndex(array $ids, array $current_path, array &$index): void {
505    foreach ($ids as $idx => $id) {
505    foreach ($ids as $idx => $id) {
505    foreach ($ids as $idx => $id) {
506      $path = [...$current_path, $idx];
507      $index[$id] = [
508        'path' => $path,
509        'parent' => $this->structure[$id]['parent'],
510      ];
511
512      $struct = $this->structure[$id];
513      $source_id = $this->nodes[$id]['source_id'];
514
515      if ($source_id === NULL) {
516        continue;
518      $class = $this->getPluginClass($source_id);
519
520      foreach ($struct['slots'] as $slot_id => $child_ids) {
520      foreach ($struct['slots'] as $slot_id => $child_ids) {
520      foreach ($struct['slots'] as $slot_id => $child_ids) {
521        if ($class && \is_subclass_of($class, SourceWithSlotsInterface::class)) {
521        if ($class && \is_subclass_of($class, SourceWithSlotsInterface::class)) {
521        if ($class && \is_subclass_of($class, SourceWithSlotsInterface::class)) {
521        if ($class && \is_subclass_of($class, SourceWithSlotsInterface::class)) {
522          $child_path = [...$path, 'source', ...$class::getSlotPath($slot_id)];
525          continue;
520      foreach ($struct['slots'] as $slot_id => $child_ids) {
521        if ($class && \is_subclass_of($class, SourceWithSlotsInterface::class)) {
522          $child_path = [...$path, 'source', ...$class::getSlotPath($slot_id)];
523        }
524        else {
525          continue;
526        }
527        $this->buildPathIndex($child_ids, $child_path, $index);
505    foreach ($ids as $idx => $id) {
506      $path = [...$current_path, $idx];
507      $index[$id] = [
508        'path' => $path,
509        'parent' => $this->structure[$id]['parent'],
510      ];
511
512      $struct = $this->structure[$id];
513      $source_id = $this->nodes[$id]['source_id'];
514
515      if ($source_id === NULL) {
516        continue;
517      }
518      $class = $this->getPluginClass($source_id);
519
520      foreach ($struct['slots'] as $slot_id => $child_ids) {
505    foreach ($ids as $idx => $id) {
506      $path = [...$current_path, $idx];
507      $index[$id] = [
508        'path' => $path,
509        'parent' => $this->structure[$id]['parent'],
510      ];
511
512      $struct = $this->structure[$id];
513      $source_id = $this->nodes[$id]['source_id'];
514
515      if ($source_id === NULL) {
516        continue;
517      }
518      $class = $this->getPluginClass($source_id);
519
520      foreach ($struct['slots'] as $slot_id => $child_ids) {
521        if ($class && \is_subclass_of($class, SourceWithSlotsInterface::class)) {
522          $child_path = [...$path, 'source', ...$class::getSlotPath($slot_id)];
523        }
524        else {
525          continue;
526        }
527        $this->buildPathIndex($child_ids, $child_path, $index);
528      }
529    }
530  }
SourceTree->denormalize
457  private function denormalize(array $ids): array {
458    return \array_map(function ($id) {
459      $node = ['node_id' => $id] + $this->nodes[$id];
460
461      return $this->injectChildren($node, $this->structure[$id]['slots']);
462    }, $ids);
463  }
SourceTree->generateNodeId
397    return \bin2hex(\random_bytes(8));
398  }
SourceTree->getNode
320  public function getNode(string $node_id): ?array {
321    if (!isset($this->nodes[$node_id])) {
322      return NULL;
325    return $this->denormalize([$node_id])[0];
326  }
SourceTree->getNodeData
307  public function getNodeData(string $node_id): ?array {
308    return $this->nodes[$node_id] ?? NULL;
309  }
SourceTree->getNormalizedStructure
103      'nodes' => $this->nodes,
104      'structure' => $this->structure,
105      'root' => $this->root,
106    ];
107  }
SourceTree->getParentId
337  public function getParentId(string $node_id): ?string {
338    return $this->structure[$node_id]['parent'] ?? NULL;
339  }
SourceTree->getPathIndex
116    if ($this->pathIndex === NULL) {
117      $this->pathIndex = [];
118      $this->buildPathIndex($this->root, [], $this->pathIndex);
119    }
120
121    return $this->pathIndex;
121    return $this->pathIndex;
122  }
SourceTree->getPluginClass
657  private function getPluginClass(string $source_id): ?string {
658    if (\array_key_exists($source_id, $this->pluginClassCache)) {
659      return $this->pluginClassCache[$source_id];
662    try {
663      $definition = $this->getSourceManager()->getDefinition($source_id);
664      $this->pluginClassCache[$source_id] = $definition['class'] ?? NULL;
666    catch (\Exception $e) {
668      \Drupal::logger('display_builder')->warning('SourceTree: failed to get definition for source plugin %id: @message', ['%id' => $source_id, '@message' => $e->getMessage()]);
669      $this->pluginClassCache[$source_id] = NULL;
670    }
671
672    return $this->pluginClassCache[$source_id];
672    return $this->pluginClassCache[$source_id];
673  }
SourceTree->getSourceManager
687    if ($this->sourceManager === NULL) {
689      $this->sourceManager = \Drupal::service('plugin.manager.ui_patterns_source');
690    }
691
692    return $this->sourceManager;
692    return $this->sourceManager;
693  }
SourceTree->getSourcePlugin
634  private function getSourcePlugin(string $source_id, array $source_configuration): ?SourceInterface {
635    try {
636      $plugin = $this->getSourceManager()->createInstance($source_id, ['settings' => $source_configuration]);
637
638      return $plugin instanceof SourceInterface ? $plugin : NULL;
638      return $plugin instanceof SourceInterface ? $plugin : NULL;
638      return $plugin instanceof SourceInterface ? $plugin : NULL;
638      return $plugin instanceof SourceInterface ? $plugin : NULL;
640    catch (\Exception $e) {
642      \Drupal::logger('display_builder')->warning('SourceTree: failed to instantiate source plugin %id: @message', ['%id' => $source_id, '@message' => $e->getMessage()]);
643
644      return NULL;
645    }
646  }
SourceTree->getTree
89    return $this->denormalize($this->root);
90  }
SourceTree->hasNode
294  public function hasNode(string $node_id): bool {
295    return isset($this->nodes[$node_id]);
296  }
SourceTree->injectChildren
476  private function injectChildren(array $node, array $slots): array {
477    if (empty($slots)) {
478      return $node;
481    $source_id = $node['source_id'] ?? '';
482    $class = $this->getPluginClass($source_id);
483
484    if ($class && \is_subclass_of($class, SourceWithSlotsInterface::class)) {
484    if ($class && \is_subclass_of($class, SourceWithSlotsInterface::class)) {
484    if ($class && \is_subclass_of($class, SourceWithSlotsInterface::class)) {
485      foreach ($slots as $slot_id => $child_ids) {
485      foreach ($slots as $slot_id => $child_ids) {
485      foreach ($slots as $slot_id => $child_ids) {
485      foreach ($slots as $slot_id => $child_ids) {
486        $path = $class::getSlotPath($slot_id);
487        NestedArray::setValue($node['source'], $path, $this->denormalize($child_ids));
488      }
489    }
490
491    return $node;
491    return $node;
492  }
SourceTree->isDescendant
610  private function isDescendant(string $node_id, string $potential_ancestor_id): bool {
611    $current_parent = $this->getParentId($node_id);
612
613    while ($current_parent !== NULL) {
614      if ($current_parent === $potential_ancestor_id) {
615        return TRUE;
613    while ($current_parent !== NULL) {
614      if ($current_parent === $potential_ancestor_id) {
615        return TRUE;
616      }
617      $current_parent = $this->getParentId($current_parent);
613    while ($current_parent !== NULL) {
620    return FALSE;
621  }
SourceTree->moveToRoot
207  public function moveToRoot(string $node_id, int $position): bool {
208    if (!$this->removeFromCurrentParent($node_id)) {
209      return FALSE;
212    $this->structure[$node_id]['parent'] = NULL;
213    $this->structure[$node_id]['slot'] = NULL;
214    \array_splice($this->root, $position, 0, [$node_id]);
215    $this->pathIndex = NULL;
216
217    return TRUE;
218  }
SourceTree->moveToSlot
235  public function moveToSlot(string $node_id, string $parent_id, string $slot_id, int $position): bool {
236    if (!isset($this->structure[$parent_id])) {
237      return FALSE;
241    if ($node_id === $parent_id) {
242      return FALSE;
246    if ($this->isDescendant($parent_id, $node_id)) {
247      return FALSE;
250    if (!$this->removeFromCurrentParent($node_id)) {
251      return FALSE;
254    $this->structure[$node_id]['parent'] = $parent_id;
255    $this->structure[$node_id]['slot'] = $slot_id;
256
257    if (!isset($this->structure[$parent_id]['slots'][$slot_id])) {
258      $this->structure[$parent_id]['slots'][$slot_id] = [];
259    }
260    \array_splice($this->structure[$parent_id]['slots'][$slot_id], $position, 0, [$node_id]);
260    \array_splice($this->structure[$parent_id]['slots'][$slot_id], $position, 0, [$node_id]);
261    $this->pathIndex = NULL;
262
263    return TRUE;
264  }
SourceTree->normalize
413  private function normalize(array $items, ?string $parent_id, ?string $slot_id): array {
414    $ids = [];
415
416    foreach ($items as $item) {
416    foreach ($items as $item) {
417      $node_id = $item['node_id'] ?? $this->generateNodeId();
418      $ids[] = $node_id;
419
420      $source_id = $item['source_id'] ?? '';
421      $plugin = $this->getSourcePlugin($source_id, $item['source'] ?? []);
422
423      $slots = [];
424
425      if ($plugin instanceof SourceWithSlotsInterface) {
426        foreach ($plugin->getSlotValues() as $child_slot_id => $data) {
426        foreach ($plugin->getSlotValues() as $child_slot_id => $data) {
426        foreach ($plugin->getSlotValues() as $child_slot_id => $data) {
426        foreach ($plugin->getSlotValues() as $child_slot_id => $data) {
427          $slots[$child_slot_id] = $this->normalize($data, $node_id, $child_slot_id);
428        }
429
430        foreach ($plugin->getSlotDefinitions() as $child_slot_id => $_) {
430        foreach ($plugin->getSlotDefinitions() as $child_slot_id => $_) {
430        foreach ($plugin->getSlotDefinitions() as $child_slot_id => $_) {
430        foreach ($plugin->getSlotDefinitions() as $child_slot_id => $_) {
431          $path = $plugin::getSlotPath($child_slot_id);
432          NestedArray::unsetValue($item['source'], $path);
433        }
434      }
435
436      unset($item['node_id']);
416    foreach ($items as $item) {
417      $node_id = $item['node_id'] ?? $this->generateNodeId();
418      $ids[] = $node_id;
419
420      $source_id = $item['source_id'] ?? '';
421      $plugin = $this->getSourcePlugin($source_id, $item['source'] ?? []);
422
423      $slots = [];
424
425      if ($plugin instanceof SourceWithSlotsInterface) {
426        foreach ($plugin->getSlotValues() as $child_slot_id => $data) {
427          $slots[$child_slot_id] = $this->normalize($data, $node_id, $child_slot_id);
428        }
429
430        foreach ($plugin->getSlotDefinitions() as $child_slot_id => $_) {
431          $path = $plugin::getSlotPath($child_slot_id);
432          NestedArray::unsetValue($item['source'], $path);
433        }
434      }
435
436      unset($item['node_id']);
416    foreach ($items as $item) {
417      $node_id = $item['node_id'] ?? $this->generateNodeId();
418      $ids[] = $node_id;
419
420      $source_id = $item['source_id'] ?? '';
421      $plugin = $this->getSourcePlugin($source_id, $item['source'] ?? []);
422
423      $slots = [];
424
425      if ($plugin instanceof SourceWithSlotsInterface) {
426        foreach ($plugin->getSlotValues() as $child_slot_id => $data) {
427          $slots[$child_slot_id] = $this->normalize($data, $node_id, $child_slot_id);
428        }
429
430        foreach ($plugin->getSlotDefinitions() as $child_slot_id => $_) {
431          $path = $plugin::getSlotPath($child_slot_id);
432          NestedArray::unsetValue($item['source'], $path);
433        }
434      }
435
436      unset($item['node_id']);
437      $this->structure[$node_id] = [
438        'parent' => $parent_id,
439        'slot' => $slot_id,
440        'slots' => $slots,
441      ];
442      $this->nodes[$node_id] = $item;
443    }
444
445    return $ids;
446  }
SourceTree->rebuild
74  public function rebuild(array $tree): void {
75    $this->nodes = [];
76    $this->structure = [];
77    $this->pluginClassCache = [];
78    $this->root = $this->normalize($tree, NULL, NULL);
79    $this->pathIndex = NULL;
80  }
SourceTree->recursiveRemove
586  private function recursiveRemove(string $node_id): void {
587    if (!isset($this->structure[$node_id])) {
588      return;
591    foreach ($this->structure[$node_id]['slots'] as $child_ids) {
591    foreach ($this->structure[$node_id]['slots'] as $child_ids) {
592      foreach ($child_ids as $child_id) {
592      foreach ($child_ids as $child_id) {
592      foreach ($child_ids as $child_id) {
593        $this->recursiveRemove($child_id);
591    foreach ($this->structure[$node_id]['slots'] as $child_ids) {
592      foreach ($child_ids as $child_id) {
591    foreach ($this->structure[$node_id]['slots'] as $child_ids) {
592      foreach ($child_ids as $child_id) {
593        $this->recursiveRemove($child_id);
594      }
595    }
596    unset($this->nodes[$node_id], $this->structure[$node_id]);
597  }
SourceTree->remove
275  public function remove(string $node_id): bool {
276    if (!$this->removeFromCurrentParent($node_id)) {
277      return FALSE;
279    $this->recursiveRemove($node_id);
280    $this->pathIndex = NULL;
281
282    return TRUE;
283  }
SourceTree->removeFromCurrentParent
541  private function removeFromCurrentParent(string $node_id): bool {
542    if (!isset($this->structure[$node_id])) {
543      return FALSE;
546    $parent_id = $this->structure[$node_id]['parent'];
547    $slot_id = $this->structure[$node_id]['slot'];
548
549    if ($parent_id === NULL) {
550      $key = \array_search($node_id, $this->root, TRUE);
551
552      if ($key === FALSE) {
553        return FALSE;
555      \array_splice($this->root, (int) $key, 1);
556
557      return TRUE;
560    if ($slot_id === NULL || !isset($this->structure[$parent_id]['slots'][$slot_id])) {
560    if ($slot_id === NULL || !isset($this->structure[$parent_id]['slots'][$slot_id])) {
560    if ($slot_id === NULL || !isset($this->structure[$parent_id]['slots'][$slot_id])) {
561      return FALSE;
564    $child_ids = &$this->structure[$parent_id]['slots'][$slot_id];
565
566    if (!\is_array($child_ids)) {
567      return FALSE;
569    $key = \array_search($node_id, $child_ids, TRUE);
570
571    if ($key !== FALSE) {
572      \array_splice($child_ids, (int) $key, 1);
573
574      return TRUE;
577    return FALSE;
578  }
SourceTree->setSource
354  public function setSource(string $node_id, string $source_id, array $source_data): bool {
355    if (!isset($this->nodes[$node_id])) {
356      return FALSE;
358    $this->nodes[$node_id]['source_id'] = $source_id;
359    $this->nodes[$node_id]['source'] = $source_data;
360
361    return TRUE;
362  }
SourceTree->setThirdPartySettings
377  public function setThirdPartySettings(string $node_id, string $island_id, array $data): bool {
378    if (!isset($this->nodes[$node_id])) {
379      return FALSE;
382    if (!isset($this->nodes[$node_id]['third_party_settings'])) {
383      $this->nodes[$node_id]['third_party_settings'] = [];
384    }
385    $this->nodes[$node_id]['third_party_settings'][$island_id] = $data;
385    $this->nodes[$node_id]['third_party_settings'][$island_id] = $data;
386
387    return TRUE;
388  }
{closure:/var/www/html/web/modules/custom/display_builder/src/SourceTree.php:458-462}
458    return \array_map(function ($id) {
459      $node = ['node_id' => $id] + $this->nodes[$id];
460
461      return $this->injectChildren($node, $this->structure[$id]['slots']);
462    }, $ids);