Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
9.27% covered (danger)
9.27%
24 / 259
13.95% covered (danger)
13.95%
6 / 43
CRAP
0.00% covered (danger)
0.00%
0 / 1
Instance
9.27% covered (danger)
9.27%
24 / 259
13.95% covered (danger)
13.95%
6 / 43
5610.60
0.00% covered (danger)
0.00%
0 / 1
 toArray
100.00% covered (success)
100.00%
9 / 9
100.00% covered (success)
100.00%
1 / 1
1
 postCreate
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
2
 getProfile
0.00% covered (danger)
0.00%
0 / 2
0.00% covered (danger)
0.00%
0 / 1
2
 setProfile
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 moveToRoot
0.00% covered (danger)
0.00%
0 / 14
0.00% covered (danger)
0.00%
0 / 1
12
 moveToSlot
0.00% covered (danger)
0.00%
0 / 23
0.00% covered (danger)
0.00%
0 / 1
30
 attachToRoot
0.00% covered (danger)
0.00%
0 / 16
0.00% covered (danger)
0.00%
0 / 1
6
 attachToSlot
0.00% covered (danger)
0.00%
0 / 19
0.00% covered (danger)
0.00%
0 / 1
6
 get
0.00% covered (danger)
0.00%
0 / 4
0.00% covered (danger)
0.00%
0 / 1
2
 getParentId
0.00% covered (danger)
0.00%
0 / 4
0.00% covered (danger)
0.00%
0 / 1
2
 setSource
0.00% covered (danger)
0.00%
0 / 13
0.00% covered (danger)
0.00%
0 / 1
12
 setThirdPartySettings
0.00% covered (danger)
0.00%
0 / 13
0.00% covered (danger)
0.00%
0 / 1
6
 remove
0.00% covered (danger)
0.00%
0 / 13
0.00% covered (danger)
0.00%
0 / 1
6
 getContexts
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 setSave
0.00% covered (danger)
0.00%
0 / 2
0.00% covered (danger)
0.00%
0 / 1
2
 getCurrentState
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 restore
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 undo
0.00% covered (danger)
0.00%
0 / 8
0.00% covered (danger)
0.00%
0 / 1
6
 redo
0.00% covered (danger)
0.00%
0 / 7
0.00% covered (danger)
0.00%
0 / 1
6
 clear
0.00% covered (danger)
0.00%
0 / 2
0.00% covered (danger)
0.00%
0 / 1
2
 getCountPast
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 getCountFuture
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 getUsers
0.00% covered (danger)
0.00%
0 / 7
0.00% covered (danger)
0.00%
0 / 1
20
 canSaveContextsRequirement
0.00% covered (danger)
0.00%
0 / 7
0.00% covered (danger)
0.00%
0 / 1
20
 hasSaveContextsRequirement
0.00% covered (danger)
0.00%
0 / 7
0.00% covered (danger)
0.00%
0 / 1
30
 hasSave
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 saveIsCurrent
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 getPathIndex
0.00% covered (danger)
0.00%
0 / 4
0.00% covered (danger)
0.00%
0 / 1
6
 setNewPresent
0.00% covered (danger)
0.00%
0 / 14
0.00% covered (danger)
0.00%
0 / 1
20
 getCurrent
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getUniqId
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 buildIndexFromSlot
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
1 / 1
2
 sampleEntityGenerator
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 slotSourceProxy
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 currentUser
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 refreshContexts
0.00% covered (danger)
0.00%
0 / 10
0.00% covered (danger)
0.00%
0 / 1
30
 changeSourcePositionInSlot
0.00% covered (danger)
0.00%
0 / 6
0.00% covered (danger)
0.00%
0 / 1
12
 getNodeId
0.00% covered (danger)
0.00%
0 / 5
0.00% covered (danger)
0.00%
0 / 1
12
 buildIndexFromSource
37.50% covered (danger)
37.50%
6 / 16
0.00% covered (danger)
0.00%
0 / 1
14.79
 doAttachToRoot
0.00% covered (danger)
0.00%
0 / 2
0.00% covered (danger)
0.00%
0 / 1
2
 doAttachToSlot
0.00% covered (danger)
0.00%
0 / 6
0.00% covered (danger)
0.00%
0 / 1
2
 doRemove
0.00% covered (danger)
0.00%
0 / 6
0.00% covered (danger)
0.00%
0 / 1
2
 getPath
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
1<?php
2
3declare(strict_types=1);
4
5namespace Drupal\display_builder\Entity;
6
7use Drupal\Component\Render\FormattableMarkup;
8use Drupal\Component\Utility\NestedArray;
9use Drupal\Core\Entity\Attribute\EntityType;
10use Drupal\Core\Entity\EntityBase;
11use Drupal\Core\Entity\EntityStorageInterface;
12use Drupal\Core\Entity\EntityTypeManagerInterface;
13use Drupal\Core\Plugin\Context\EntityContext;
14use Drupal\Core\Session\AccountInterface;
15use Drupal\Core\StringTranslation\TranslatableMarkup;
16use Drupal\display_builder\HistoryStep;
17use Drupal\display_builder\InstanceAccessControlHandler;
18use Drupal\display_builder\InstanceInterface;
19use Drupal\display_builder\InstanceStorage;
20use Drupal\display_builder\ProfileInterface;
21use Drupal\display_builder\SlotSourceProxy;
22use Drupal\display_builder_ui\InstanceListBuilder;
23use Drupal\ui_patterns\Entity\SampleEntityGeneratorInterface;
24use Drupal\ui_patterns\Plugin\Context\RequirementsContext;
25
26/**
27 * Defines the display builder instance entity class.
28 */
29#[EntityType(
30  id: 'display_builder_instance',
31  label: new TranslatableMarkup('Display Builder instance'),
32  label_collection: new TranslatableMarkup('Display builder instances'),
33  label_singular: new TranslatableMarkup('display builder instance'),
34  label_plural: new TranslatableMarkup('display builder instances'),
35  entity_keys: [
36    'label' => 'id',
37  ],
38  handlers: [
39    'access' => InstanceAccessControlHandler::class,
40    'storage' => InstanceStorage::class,
41    // Managed by display_builder_ui.
42    'list_builder' => InstanceListBuilder::class,
43  ],
44  links: [
45    // Managed by display_builder_ui.
46    'collection' => '/admin/structure/display-builder/instances',
47  ],
48  label_count: [
49    'singular' => '@count instance',
50    'plural' => '@count instances',
51  ],
52)]
53class Instance extends EntityBase implements InstanceInterface {
54
55  private const MAX_HISTORY = 10;
56
57  /**
58   * Entity ID.
59   */
60  protected string $id;
61
62  /**
63   * Entity label.
64   */
65  protected string $label;
66
67  /**
68   * Display Builder profile ID.
69   */
70  protected string $profileId = '';
71
72  /**
73   * Past steps.
74   *
75   * @var \Drupal\display_builder\HistoryStep[]
76   */
77  protected array $past = [];
78
79  /**
80   * Present step.
81   */
82  protected ?HistoryStep $present = NULL;
83
84  /**
85   * Future steps.
86   *
87   * @var \Drupal\display_builder\HistoryStep[]
88   */
89  protected array $future = [];
90
91  /**
92   * Contexts.
93   *
94   * @var \Drupal\Core\Plugin\Context\ContextInterface[]
95   *   An array of contexts, keyed by context name.
96   */
97  protected array $contexts = [];
98
99  /**
100   * Saved step.
101   */
102  protected ?HistoryStep $save = NULL;
103
104  /**
105   * Path index.
106   *
107   * A mapping where each key is an slot source node ID and each value is
108   * the path where this source is located in the data state.
109   */
110  protected array $pathIndex = [];
111
112  /**
113   * Entity type manager.
114   */
115  protected EntityTypeManagerInterface $entityTypeManager;
116
117  /**
118   * Sample entity generator.
119   */
120  protected SampleEntityGeneratorInterface $sampleEntityGenerator;
121
122  /**
123   * Slot source proxy.
124   */
125  protected SlotSourceProxy $slotSourceProxy;
126
127  /**
128   * Current user.
129   */
130  protected AccountInterface $currentUser;
131
132  /**
133   * {@inheritdoc}
134   *
135   * @see \Drupal\Core\Entity\EntityInterface
136   */
137  public function toArray(): array {
138    return [
139      'id' => $this->id,
140      'profileId' => $this->profileId,
141      'contexts' => $this->contexts,
142      'past' => $this->past,
143      'present' => $this->present,
144      'future' => $this->future,
145      'save' => $this->save,
146    ];
147  }
148
149  /**
150   * {@inheritdoc}
151   *
152   * @see \Drupal\Core\Entity\EntityInterface
153   */
154  public function postCreate(EntityStorageInterface $storage): void {
155    if ($this->present) {
156      $this->present->data = $this->buildIndexFromSlot([], $this->getCurrentState());
157    }
158  }
159
160  /**
161   * {@inheritdoc}
162   */
163  public function getProfile(): ?ProfileInterface {
164    /** @var \Drupal\display_builder\ProfileInterface $profile */
165    $profile = $this->entityTypeManager()->getStorage('display_builder_profile')->load($this->profileId);
166
167    return $profile;
168  }
169
170  /**
171   * {@inheritdoc}
172   */
173  public function setProfile(string $profile_id): void {
174    $this->profileId = $profile_id;
175  }
176
177  /**
178   * {@inheritdoc}
179   */
180  public function moveToRoot(string $node_id, int $position): bool {
181    $root = $this->getCurrentState();
182    $path = $this->getPath($root, $node_id);
183    $data = NestedArray::getValue($root, $path);
184
185    if (empty($data) || !isset($data['source_id'])) {
186      return FALSE;
187    }
188
189    $root = $this->doRemove($root, $node_id);
190    $root = $this->doAttachToRoot($root, $position, $data);
191
192    // Get friendly label to display in log instead of ids.
193    $labelWithSummary = $this->slotSourceProxy()->getLabelWithSummary($data, $this->getContexts());
194
195    $log = new FormattableMarkup('%node @thingy has been moved to root', [
196      '%node' => $labelWithSummary['summary'],
197      '@thingy' => $data['source_id'],
198    ]);
199    $this->setNewPresent($root, $log);
200
201    return TRUE;
202  }
203
204  /**
205   * {@inheritdoc}
206   */
207  public function moveToSlot(string $node_id, string $parent_id, string $slot_id, int $position): bool {
208    $root = $this->getCurrentState();
209    $path = $this->getPath($root, $node_id);
210    $data = NestedArray::getValue($root, $path);
211
212    if (empty($data) || !isset($data['source_id'])) {
213      return FALSE;
214    }
215
216    $parent_slot = \array_slice($path, \count($path) - 3, 1)[0];
217
218    if (($parent_id === $this->getParentId($root, $node_id)) && ($slot_id === $parent_slot)) {
219      // Moving to the same slot is tricky, because we don't want to remove a
220      // sibling.
221      $slot_path = \array_slice($path, 0, \count($path) - 1);
222      $slot = NestedArray::getValue($root, $slot_path);
223      $slot = $this->changeSourcePositionInSlot($slot, $node_id, $position);
224      NestedArray::setValue($root, $slot_path, $slot);
225    }
226    else {
227      // Moving to a different slot is easier, we can first delete the previous
228      // node data, and attach it to the new position.
229      $root = $this->doRemove($root, $node_id);
230      $root = $this->doAttachToSlot($root, $parent_id, $slot_id, $position, $data);
231    }
232
233    // Get friendly label to display in log instead of ids.
234    $labelWithSummary = $this->slotSourceProxy()->getLabelWithSummary($data, $this->getContexts());
235    $labelWithSummaryParent = $this->slotSourceProxy()->getLabelWithSummary($this->get($parent_id));
236
237    $log = new FormattableMarkup("%node @thingy has been moved to %parent's @slot_id", [
238      '%node' => $labelWithSummary['summary'],
239      '@thingy' => $data['source_id'],
240      '%parent' => $labelWithSummaryParent['summary'],
241      '@slot_id' => $slot_id,
242    ]);
243
244    $this->setNewPresent($root, $log);
245
246    return TRUE;
247  }
248
249  /**
250   * {@inheritdoc}
251   */
252  public function attachToRoot(int $position, string $source_id, array $data, array $third_party_settings = []): string {
253    $data = [
254      '_node_id' => \uniqid(),
255      'source_id' => $source_id,
256      'source' => $data,
257    ];
258
259    if ($third_party_settings) {
260      $data['_third_party_settings'] = $third_party_settings;
261    }
262
263    $root = $this->getCurrentState();
264    $root = $this->doAttachToRoot($root, $position, $data);
265
266    // Get friendly label to display in log instead of ids.
267    $labelWithSummary = $this->slotSourceProxy()->getLabelWithSummary($data, $this->getContexts() ?? []);
268
269    $log = new FormattableMarkup('%node @source_id has been attached to root', [
270      '%node' => $labelWithSummary['summary'],
271      '@source_id' => $source_id,
272    ]);
273    $this->setNewPresent($root, $log, FALSE);
274
275    return $data['_node_id'];
276  }
277
278  /**
279   * {@inheritdoc}
280   */
281  public function attachToSlot(string $parent_id, string $slot_id, int $position, string $source_id, array $data, array $third_party_settings = []): string {
282    $root = $this->getCurrentState();
283    $data = [
284      '_node_id' => \uniqid(),
285      'source_id' => $source_id,
286      'source' => $data,
287    ];
288
289    if ($third_party_settings) {
290      $data['_third_party_settings'] = $third_party_settings;
291    }
292
293    $root = $this->doAttachToSlot($root, $parent_id, $slot_id, $position, $data);
294
295    // Get friendly label to display in log instead of ids.
296    $labelWithSummary = $this->slotSourceProxy()->getLabelWithSummary($data, $this->getContexts() ?? []);
297    $labelWithSummaryParent = $this->slotSourceProxy()->getLabelWithSummary($this->get($parent_id));
298
299    $log = new FormattableMarkup("%node @source_id has been attached to %parent's @slot_id", [
300      '%node' => $labelWithSummary['summary'],
301      '@source_id' => $source_id,
302      '%parent' => $labelWithSummaryParent['summary'],
303      '@slot_id' => $slot_id,
304    ]);
305    $this->setNewPresent($root, $log);
306
307    return $data['_node_id'];
308  }
309
310  /**
311   * {@inheritdoc}
312   */
313  public function get(string $node_id): array {
314    $root = $this->getCurrentState();
315    $path = $this->getPath($root, $node_id);
316    $value = NestedArray::getValue($root, $path);
317
318    return $value ?? [];
319  }
320
321  /**
322   * {@inheritdoc}
323   */
324  public function getParentId(array $root, string $node_id): string {
325    $path = $this->getPath($root, $node_id);
326    $length = \count(['source', 'component', 'slots', '{slot_id}', 'sources', '{position}']);
327    $parent_path = \array_slice($path, 0, \count($path) - $length);
328
329    return $this->getNodeId($parent_path);
330  }
331
332  /**
333   * {@inheritdoc}
334   */
335  public function setSource(string $node_id, string $source_id, array $data): void {
336    $root = $this->getCurrentState();
337    $path = $this->getPath($root, $node_id);
338    $existing_data = NestedArray::getValue($root, $path) ?? [];
339
340    if (!isset($existing_data['_node_id']) || ($existing_data['_node_id'] !== $node_id)) {
341      throw new \Exception('Node ID mismatch');
342    }
343    $existing_data['source_id'] = $source_id;
344    $existing_data['source'] = $data;
345    NestedArray::setValue($root, $path, $existing_data);
346
347    // Get friendly label to display in log instead of ids.
348    $labelWithSummary = $this->slotSourceProxy()->getLabelWithSummary($existing_data, $this->getContexts());
349
350    $log = new FormattableMarkup('%source has been updated', [
351      '%source' => $labelWithSummary['summary'],
352    ]);
353    $this->setNewPresent($root, $log);
354  }
355
356  /**
357   * {@inheritdoc}
358   */
359  public function setThirdPartySettings(string $node_id, string $island_id, array $data): void {
360    $root = $this->getCurrentState();
361    $path = $this->getPath($root, $node_id);
362    $existing_data = NestedArray::getValue($root, $path);
363
364    if (!isset($existing_data['_third_party_settings'])) {
365      $existing_data['_third_party_settings'] = [];
366    }
367    $existing_data['_third_party_settings'][$island_id] = $data;
368    NestedArray::setValue($root, $path, $existing_data);
369
370    // Get friendly label to display in log instead of ids.
371    $labelWithSummary = $this->slotSourceProxy()->getLabelWithSummary($existing_data, $this->getContexts());
372
373    $log = new FormattableMarkup('%source has been updated by @island_id', [
374      '%source' => $labelWithSummary['summary'],
375      '@island_id' => $island_id,
376    ]);
377    $this->setNewPresent($root, $log);
378  }
379
380  /**
381   * {@inheritdoc}
382   */
383  public function remove(string $node_id): void {
384    $root = $this->getCurrentState();
385    $path = $this->getPath($root, $node_id);
386    $data = NestedArray::getValue($root, $path);
387    $parent_id = $this->getParentId($root, $node_id);
388    $root = $this->doRemove($root, $node_id);
389
390    $contexts = $this->getContexts() ?? [];
391
392    // Get friendly label to display in log instead of ids.
393    $labelWithSummary = $this->slotSourceProxy()->getLabelWithSummary($data, $contexts);
394    $labelWithSummaryParent = empty($parent_id) ? ['summary' => 'root'] : $this->slotSourceProxy()->getLabelWithSummary($this->get($parent_id), $contexts);
395
396    $log = new FormattableMarkup('%node has been removed from %parent', [
397      '%node' => $labelWithSummary['summary'],
398      '%parent' => $labelWithSummaryParent['summary'],
399    ]);
400    $this->setNewPresent($root, $log, FALSE);
401  }
402
403  /**
404   * {@inheritdoc}
405   */
406  public function getContexts(): ?array {
407    return $this->refreshContexts($this->contexts);
408  }
409
410  /**
411   * {@inheritdoc}
412   */
413  public function setSave(array $save_data): void {
414    $hash = self::getUniqId($save_data);
415    $this->save = new HistoryStep($save_data, $hash, NULL, \time(), NULL);
416  }
417
418  /**
419   * {@inheritdoc}
420   *
421   * @see \Drupal\display_builder\HistoryInterface
422   */
423  public function getCurrentState(): array {
424    return $this->getCurrent()->data ?? [];
425  }
426
427  /**
428   * {@inheritdoc}
429   */
430  public function restore(): void {
431    $this->setNewPresent($this->save->data, 'Back to saved data.');
432  }
433
434  /**
435   * {@inheritdoc}
436   *
437   * @see \Drupal\display_builder\HistoryInterface
438   */
439  public function undo(): void {
440    $past = $this->past ?? [];
441
442    if (empty($past)) {
443      return;
444    }
445
446    $present = $this->present;
447    // Remove the last element from the past.
448    $last = \array_pop($past);
449    $this->past = $past;
450    // Set the present to the element we removed in the previous step.
451    $this->present = $last;
452    // Insert the old present state at the beginning of the future.
453    $this->future = \array_merge([$present], $this->future);
454  }
455
456  /**
457   * {@inheritdoc}
458   *
459   * @see \Drupal\display_builder\HistoryInterface
460   */
461  public function redo(): void {
462    $future = $this->future ?? [];
463
464    if (empty($future)) {
465      return;
466    }
467
468    // Remove the first element from the future.
469    $first = \array_shift($future);
470    // Insert the old present state at the end of the past.
471    $this->past = \array_merge($this->past, [$this->present]);
472    // Set the present to the element we removed in the previous step.
473    $this->present = $first;
474    $this->future = $future;
475  }
476
477  /**
478   * {@inheritdoc}
479   *
480   * @see \Drupal\display_builder\HistoryInterface
481   */
482  public function clear(): void {
483    $this->past = [];
484    $this->future = [];
485  }
486
487  /**
488   * {@inheritdoc}
489   *
490   * @see \Drupal\display_builder\HistoryInterface
491   */
492  public function getCountPast(): int {
493    return \count($this->past);
494  }
495
496  /**
497   * {@inheritdoc}
498   *
499   * @see \Drupal\display_builder\HistoryInterface
500   */
501  public function getCountFuture(): int {
502    return \count($this->future);
503  }
504
505  /**
506   * {@inheritdoc}
507   */
508  public function getUsers(): array {
509    $users = [];
510    $steps = \array_merge($this->past, [$this->present], $this->future);
511
512    foreach ($steps as $step) {
513      $user_id = $step->user ?? NULL;
514
515      if ($user_id && ($users[$user_id] ?? $step->time > 0)) {
516        $users[$user_id] = $step->time;
517      }
518    }
519
520    return $users;
521  }
522
523  /**
524   * {@inheritdoc}
525   */
526  public function canSaveContextsRequirement(?array $contexts = NULL): bool {
527    $contexts ??= $this->getContexts();
528
529    if ($contexts === NULL) {
530      return FALSE;
531    }
532
533    if (!\array_key_exists('context_requirements', $contexts)
534      || !($contexts['context_requirements'] instanceof RequirementsContext)) {
535      return FALSE;
536    }
537
538    return TRUE;
539  }
540
541  /**
542   * {@inheritdoc}
543   */
544  public function hasSaveContextsRequirement(string $key, array $contexts = []): bool {
545    $contexts = empty($contexts) ? $this->getContexts() : $contexts;
546    // Some strange edge cases where context is null.
547    $contexts ??= [];
548
549    if (!\array_key_exists('context_requirements', $contexts)
550      || !($contexts['context_requirements'] instanceof RequirementsContext)
551      || !$contexts['context_requirements']->hasValue($key)) {
552      return FALSE;
553    }
554
555    return TRUE;
556  }
557
558  /**
559   * {@inheritdoc}
560   */
561  public function hasSave(): bool {
562    return !empty($this->save);
563  }
564
565  /**
566   * {@inheritdoc}
567   */
568  public function saveIsCurrent(): bool {
569    return $this->present->hash === $this->save->hash;
570  }
571
572  /**
573   * {@inheritdoc}
574   */
575  public function getPathIndex(array $root = []): array {
576    if (empty($root)) {
577      // When called from the outside, root is not already retrieved.
578      // When called from an other method, it is better to pass an already
579      // retrieved root, for performance.
580      $root = $this->getCurrentState();
581    }
582    // It may be slow to rebuild the index every time we request it. But it is
583    // very difficult to maintain an index synchronized with the state storage
584    // history.
585    $this->buildIndexFromSlot([], $root);
586
587    return $this->pathIndex ?? [];
588  }
589
590  /**
591   * {@inheritdoc}
592   *
593   * @see \Drupal\display_builder\HistoryInterface
594   */
595  public function setNewPresent(array $data, FormattableMarkup|string $log_message = '', bool $check_hash = TRUE): void {
596    $hash = self::getUniqId($data);
597
598    // Check if this present is the same to avoid duplicates, for example move
599    // to the same place.
600    if ($check_hash && $hash === $this->present?->hash) {
601      return;
602    }
603
604    // 1. Insert the present at the end of the past.
605    $this->past[] = $this->present;
606
607    // Keep only the last x history.
608    if (\count($this->past) > self::MAX_HISTORY) {
609      \array_shift($this->past);
610    }
611
612    // 2. Set the present to the new state.
613    $this->present = new HistoryStep(
614      $data,
615      $hash,
616      $log_message,
617      \time(),
618      (int) $this->currentUser()->id(),
619    );
620
621    // 3. Clear the future.
622    $this->future = [];
623  }
624
625  /**
626   * {@inheritdoc}
627   *
628   * @see \Drupal\display_builder\HistoryInterface
629   */
630  public function getCurrent(): ?HistoryStep {
631    return $this->present;
632  }
633
634  /**
635   * {@inheritdoc}
636   */
637  public static function getUniqId(array $data): int {
638    return \crc32((string) \serialize($data));
639  }
640
641  /**
642   * Build the index from a slot.
643   *
644   * @param array $path
645   *   The path to the slot.
646   * @param array $data
647   *   (Optional) The slot data.
648   *
649   * @return array
650   *   The slot data with the index updated.
651   */
652  private function buildIndexFromSlot(array $path, array $data = []): array {
653    foreach ($data as $index => $source) {
654      $source_path = \array_merge($path, [$index]);
655      $data[$index] = $this->buildIndexFromSource($source_path, $source);
656    }
657
658    return $data;
659  }
660
661  /**
662   * Sample entity generator.
663   */
664  private function sampleEntityGenerator(): SampleEntityGeneratorInterface {
665    return $this->sampleEntityGenerator ??= \Drupal::service('ui_patterns.sample_entity_generator');
666  }
667
668  /**
669   * Slot source proxy.
670   */
671  private function slotSourceProxy(): SlotSourceProxy {
672    return $this->slotSourceProxy ??= \Drupal::service('display_builder.slot_sources_proxy');
673  }
674
675  /**
676   * Slot source proxy.
677   */
678  private function currentUser(): AccountInterface {
679    return $this->currentUser ??= \Drupal::service('current_user');
680  }
681
682  /**
683   * Refresh contexts after loaded from storage.
684   *
685   * @param \Drupal\Core\Plugin\Context\ContextInterface[] $contexts
686   *   The contexts.
687   *
688   * @throws \Drupal\Component\Plugin\Exception\ContextException
689   * @throws \Drupal\Component\Plugin\Exception\InvalidPluginDefinitionException
690   * @throws \Drupal\Component\Plugin\Exception\PluginNotFoundException
691   *
692   * @return array
693   *   The refreshed contexts or NULL if no context.
694   */
695  private function refreshContexts(array $contexts): array {
696    foreach ($contexts as &$context) {
697      if ($context instanceof EntityContext) {
698        // @todo We should use cache entries here
699        // with the corresponding cache contexts in it.
700        // This may avoid some unnecessary entity loads or generation.
701        $entity = $context->getContextValue();
702
703        // Check if sample entity.
704        if ($entity->id()) {
705          $entity = $this->entityTypeManager()->getStorage($entity->getEntityTypeId())->load($entity->id());
706        }
707        else {
708          $entity = $this->sampleEntityGenerator()->get($entity->getEntityTypeId(), $entity->bundle());
709        }
710
711        // Edge case when the parent entity is deleted but not the builder
712        // instance.
713        if (!$entity) {
714          return $contexts;
715        }
716        $context = (\get_class($context))::fromEntity($entity);
717      }
718    }
719
720    return $contexts;
721  }
722
723  /**
724   * Change the position of an source in a slot.
725   *
726   * @param array $slot
727   *   The slot.
728   * @param string $node_id
729   *   The node id of the source.
730   * @param int $to
731   *   The new position.
732   *
733   * @return array
734   *   The updated slot.
735   */
736  private function changeSourcePositionInSlot(array $slot, string $node_id, int $to): array {
737    foreach ($slot as $position => $source) {
738      if ($source['_node_id'] === $node_id) {
739        $p1 = \array_splice($slot, $position, 1);
740        $p2 = \array_splice($slot, 0, $to);
741
742        return \array_merge($p2, $p1, $slot);
743      }
744    }
745
746    return $slot;
747  }
748
749  /**
750   * Get the source nod ID from a path.
751   *
752   * @todo may be slow.
753   *
754   * @param array $path
755   *   The path to the slot.
756   */
757  private function getNodeId(array $path): string {
758    $index = $this->getPathIndex();
759
760    foreach ($index as $node_id => $node_path) {
761      if ($path === $node_path) {
762        return $node_id;
763      }
764    }
765
766    return '';
767  }
768
769  /**
770   * Add path to index and add node ID to source.
771   *
772   * @param array $path
773   *   The path to the slot.
774   * @param array $data
775   *   (Optional) The slot data.
776   *
777   * @return array
778   *   The slot data with the index updated.
779   */
780  private function buildIndexFromSource(array $path, array $data = []): array {
781    // First job: Add missing _node_id keys.
782    $node_id = $data['_node_id'] ?? \uniqid();
783    $data['_node_id'] = $node_id;
784    // Second job: Save the path to the index.
785    $this->pathIndex[$node_id] = $path;
786
787    if (!isset($data['source_id'])) {
788      return $data;
789    }
790
791    // Let's continue the exploration.
792    if ($data['source_id'] !== 'component') {
793      return $data;
794    }
795
796    if (!isset($data['source']['component']['slots'])) {
797      return $data;
798    }
799
800    foreach ($data['source']['component']['slots'] as $slot_id => $slot) {
801      if (!isset($slot['sources'])) {
802        continue;
803      }
804      $slot_path = \array_merge($path, ['source', 'component', 'slots', $slot_id, 'sources']);
805      $slot['sources'] = $this->buildIndexFromSlot($slot_path, $slot['sources']);
806      $data['source']['component']['slots'][$slot_id] = $slot;
807    }
808
809    return $data;
810  }
811
812  /**
813   * Internal atomic change of the root state.
814   *
815   * @param array $root
816   *   The root state.
817   * @param int $position
818   *   The position where to insert the data.
819   * @param array $data
820   *   The data to insert.
821   *
822   * @return array
823   *   The updated root state
824   */
825  private function doAttachToRoot(array $root, int $position, array $data): array {
826    \array_splice($root, $position, 0, [$data]);
827
828    return $root;
829  }
830
831  /**
832   * Internal atomic change of the root state.
833   *
834   * @param array $root
835   *   The root state.
836   * @param string $parent_id
837   *   The ID of the parent node.
838   * @param string $slot_id
839   *   The ID of the slot where to insert the data.
840   * @param int $position
841   *   The position where to insert the data.
842   * @param array $data
843   *   The data to insert.
844   *
845   * @return array
846   *   The updated root state
847   */
848  private function doAttachToSlot(array $root, string $parent_id, string $slot_id, int $position, array $data): array {
849    $parent_path = $this->getPath($root, $parent_id);
850    $slot_path = \array_merge($parent_path, ['source', 'component', 'slots', $slot_id, 'sources']);
851    $slot = NestedArray::getValue($root, $slot_path) ?? [];
852    \array_splice($slot, $position, 0, [$data]);
853    NestedArray::setValue($root, $slot_path, $slot);
854
855    return $root;
856  }
857
858  /**
859   * Internal atomic change of the root state.
860   *
861   * @param array $root
862   *   The root state.
863   * @param string $node_id
864   *   The node id of the source.
865   *
866   * @return array
867   *   The updated root state
868   */
869  private function doRemove(array $root, string $node_id): array {
870    $path = $this->getPath($root, $node_id);
871    NestedArray::unsetValue($root, $path);
872    // To avoid non consecutive array keys, we rebuild the value list.
873    $slot_path = \array_slice($path, 0, \count($path) - 1);
874    $slot = NestedArray::getValue($root, $slot_path);
875    NestedArray::setValue($root, $slot_path, \array_values($slot));
876
877    return $root;
878  }
879
880  /**
881   * Get the path to an source.
882   *
883   * @param array $root
884   *   The root state.
885   * @param string $node_id
886   *   The node id of the source.
887   *
888   * @return array
889   *   The path, one array item by level.
890   */
891  private function getPath(array $root, string $node_id): array {
892    return $this->getPathIndex($root)[$node_id] ?? [];
893  }
894
895}