Code Coverage
 
Lines
Branches
Paths
Functions and Methods
Classes and Traits
Total
91.09% covered (success)
91.09%
225 / 247
77.30% covered (warning)
77.30%
109 / 141
50.00% covered (danger)
50.00%
60 / 120
70.73% covered (warning)
70.73%
29 / 41
CRAP
0.00% covered (danger)
0.00%
0 / 1
Instance
91.09% covered (success)
91.09%
225 / 247
77.30% covered (warning)
77.30%
109 / 141
50.00% covered (danger)
50.00%
60 / 120
73.17% covered (warning)
73.17%
30 / 41
1033.12
0.00% covered (danger)
0.00%
0 / 1
 __construct
60.00% covered (warning)
60.00%
3 / 5
50.00% covered (danger)
50.00%
3 / 6
25.00% covered (danger)
25.00%
1 / 4
0.00% covered (danger)
0.00%
0 / 1
6.80
 baseFieldDefinitions
100.00% covered (success)
100.00%
9 / 9
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
 isNew
0.00% covered (danger)
0.00%
0 / 1
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
 label
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
 toArray
100.00% covered (success)
100.00%
9 / 9
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
 postCreate
100.00% covered (success)
100.00%
13 / 13
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
 getProfile
100.00% covered (success)
100.00%
3 / 3
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
 setProfile
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
 moveToRoot
84.62% covered (warning)
84.62%
11 / 13
60.00% covered (warning)
60.00%
3 / 5
33.33% covered (danger)
33.33%
1 / 3
0.00% covered (danger)
0.00%
0 / 1
5.67
 moveToSlot
87.50% covered (warning)
87.50%
14 / 16
60.00% covered (warning)
60.00%
3 / 5
33.33% covered (danger)
33.33%
1 / 3
0.00% covered (danger)
0.00%
0 / 1
5.67
 attachToRoot
84.62% covered (warning)
84.62%
11 / 13
33.33% covered (danger)
33.33%
2 / 6
25.00% covered (danger)
25.00%
1 / 4
0.00% covered (danger)
0.00%
0 / 1
6.80
 attachToSlot
88.89% covered (warning)
88.89%
16 / 18
50.00% covered (danger)
50.00%
4 / 8
40.00% covered (danger)
40.00%
2 / 5
0.00% covered (danger)
0.00%
0 / 1
7.46
 getNode
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
 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%
8 / 8
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%
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
 remove
100.00% covered (success)
100.00%
14 / 14
100.00% covered (success)
100.00%
6 / 6
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
3
 getContexts
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
 setSave
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
 getCurrentState
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
 restore
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
 undo
100.00% covered (success)
100.00%
12 / 12
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
 redo
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
 clear
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
 isHistoryNew
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 5
0.00% covered (danger)
0.00%
0 / 4
0.00% covered (danger)
0.00%
0 / 1
12
 getCountPast
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
 getCountFuture
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
 getUsers
88.89% covered (warning)
88.89%
8 / 9
83.33% covered (warning)
83.33%
10 / 12
11.11% covered (danger)
11.11%
1 / 9
0.00% covered (danger)
0.00%
0 / 1
31.28
 canSaveContextsRequirement
85.71% covered (warning)
85.71%
6 / 7
85.71% covered (warning)
85.71%
6 / 7
40.00% covered (danger)
40.00%
2 / 5
0.00% covered (danger)
0.00%
0 / 1
7.46
 hasSaveContextsRequirement
100.00% covered (success)
100.00%
7 / 7
90.00% covered (success)
90.00%
9 / 10
18.75% covered (danger)
18.75%
3 / 16
100.00% covered (success)
100.00%
1 / 1
18.41
 hasSave
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
 saveIsCurrent
100.00% covered (success)
100.00%
7 / 7
100.00% covered (success)
100.00%
7 / 7
33.33% covered (danger)
33.33%
2 / 6
100.00% covered (success)
100.00%
1 / 1
8.74
 getPathIndex
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
 setNewPresent
100.00% covered (success)
100.00%
19 / 19
100.00% covered (success)
100.00%
11 / 11
56.25% covered (warning)
56.25%
9 / 16
100.00% covered (success)
100.00%
1 / 1
9.01
 getCurrent
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
 getUniqId
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
 sampleEntityGenerator
0.00% covered (danger)
0.00%
0 / 1
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
 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
 currentUser
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
 refreshContexts
30.00% covered (danger)
30.00%
3 / 10
45.45% covered (danger)
45.45%
5 / 11
14.29% covered (danger)
14.29%
1 / 7
0.00% covered (danger)
0.00%
0 / 1
20.74
 getPath
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
1<?php
2
3declare(strict_types=1);
4
5namespace Drupal\display_builder\Entity;
6
7use Drupal\Component\Render\FormattableMarkup;
8use Drupal\Component\Utility\NestedArray;
9use Drupal\Core\Entity\Attribute\ContentEntityType;
10use Drupal\Core\Entity\ContentEntityBase;
11use Drupal\Core\Entity\EntityStorageInterface;
12use Drupal\Core\Entity\EntityTypeInterface;
13use Drupal\Core\Entity\EntityTypeManagerInterface;
14use Drupal\Core\Field\BaseFieldDefinition;
15use Drupal\Core\Plugin\Context\EntityContext;
16use Drupal\Core\Session\AccountInterface;
17use Drupal\Core\StringTranslation\TranslatableMarkup;
18use Drupal\display_builder\InstanceAccessControlHandler;
19use Drupal\display_builder\InstanceInterface;
20use Drupal\display_builder\InstanceStorage;
21use Drupal\display_builder\Plugin\Field\FieldType\HistoryStep;
22use Drupal\display_builder\ProfileInterface;
23use Drupal\display_builder\SlotSourceProxy;
24use Drupal\display_builder\SourceTree;
25use Drupal\display_builder_ui\InstanceListBuilder;
26use Drupal\ui_patterns\Entity\SampleEntityGeneratorInterface;
27use Drupal\ui_patterns\Plugin\Context\RequirementsContext;
28use Drupal\ui_patterns\SourcePluginManager;
29
30/**
31 * Defines the display builder instance entity class.
32 */
33#[ContentEntityType(
34  id: 'display_builder_instance',
35  label: new TranslatableMarkup('Display Builder instance'),
36  label_collection: new TranslatableMarkup('Display builder instances'),
37  label_singular: new TranslatableMarkup('display builder instance'),
38  label_plural: new TranslatableMarkup('display builder instances'),
39  entity_keys: [
40    'id' => 'id',
41  ],
42  handlers: [
43    'access' => InstanceAccessControlHandler::class,
44    'storage' => InstanceStorage::class,
45    // Managed by display_builder_ui.
46    'list_builder' => InstanceListBuilder::class,
47  ],
48  links: [
49    // Managed by display_builder_ui.
50    'collection' => '/admin/structure/display-builder/instances',
51  ],
52  label_count: [
53    'singular' => '@count instance',
54    'plural' => '@count instances',
55  ],
56)]
57class Instance extends ContentEntityBase implements InstanceInterface {
58
59  private const MAX_HISTORY = 10;
60
61  /**
62   * Current user.
63   */
64  public AccountInterface $currentUser;
65
66  /**
67   * Path index.
68   *
69   * A mapping where each key is an slot source node ID and each value has
70   * two properties:
71   * - path: the path
72   * - parent: the node ID of the parent. This is necessary because not every
73   *   SourceWithSlotsInterface implementations has the same "deepness". For
74   *   example, ComponentSource has 4 levels (component, slots, slot_id,
75   *   'sources), LayoutSource has 2 levels (regions, slot_id), etc.
76   */
77  protected array $pathIndex = [];
78
79  /**
80   * Entity type manager.
81   */
82  protected EntityTypeManagerInterface $entityTypeManager;
83
84  /**
85   * Sample entity generator.
86   */
87  protected SampleEntityGeneratorInterface $sampleEntityGenerator;
88
89  /**
90   * Slot source proxy.
91   */
92  protected SlotSourceProxy $slotSourceProxy;
93
94  /**
95   * Source plugin manager.
96   */
97  protected SourcePluginManager $sourceManager;
98
99  /**
100   * {@inheritdoc}
101   */
102  public function __construct(array $values, mixed $entity_type, mixed $bundle = FALSE, mixed $translations = []) {
103    parent::__construct($values, $entity_type, $bundle, $translations);
104    $fields = $this->fieldDefinitions;
105
106    foreach ($values as $key => $value) {
107      if (isset($fields[$key])) {
108        $this->set($key, $value);
109      }
110    }
111  }
112
113  /**
114   * {@inheritdoc}
115   */
116  public static function baseFieldDefinitions(EntityTypeInterface $entity_type) {
117    $fields = parent::baseFieldDefinitions($entity_type);
118    // Override from ContentEntityBase.
119    $fields['id'] = BaseFieldDefinition::create('string');
120    // @todo replace by entity_reference.
121    $fields['profileId'] = BaseFieldDefinition::create('string');
122    $fields['contexts'] = BaseFieldDefinition::create('map');
123    // @todo replace by Revisions API.
124    $fields['present'] = BaseFieldDefinition::create('step');
125    $fields['past'] = BaseFieldDefinition::create('step')->setCardinality(-1);
126    $fields['future'] = BaseFieldDefinition::create('step')->setCardinality(-1);
127    $fields['save'] = BaseFieldDefinition::create('step');
128
129    return $fields;
130  }
131
132  /**
133   * {@inheritdoc}
134   */
135  public function isNew(): bool {
136    // We don't support enforceIsNew property because we have no practical
137    // use of it and because it seems to break the invalidation of
138    // ::getCacheTags().
139    return !$this->id();
140  }
141
142  /**
143   * {@inheritdoc}
144   */
145  public function label() {
146    // Extract a human readable name from an instance id.
147    // Example: "provider__my_display" -> "My display".
148    $parts = \explode('__', (string) $this->id());
149
150    if (\count($parts) > 1) {
151      \array_shift($parts);
152
153      return \ucfirst(\implode(' ', \str_replace('_', ' ', $parts)));
154    }
155
156    return (string) $this->id();
157  }
158
159  /**
160   * {@inheritdoc}
161   *
162   * @see \Drupal\Core\Entity\EntityInterface
163   */
164  public function toArray(): array {
165    return [
166      'id' => $this->id(),
167      'profileId' => $this->get('profileId')->getString(),
168      'contexts' => $this->get('contexts')->first()?->getValue(),
169      'past' => $this->get('past')->getValue(),
170      'present' => $this->get('present')->first()?->getValue(),
171      'future' => $this->get('future')->getValue(),
172      'save' => $this->get('save')->first()?->getValue(),
173    ];
174  }
175
176  /**
177   * {@inheritdoc}
178   *
179   * @see \Drupal\Core\Entity\EntityInterface
180   */
181  public function postCreate(EntityStorageInterface $storage): void {
182    if ($this->get('present')->isEmpty()) {
183      return;
184    }
185    /** @var \Drupal\display_builder\Plugin\Field\FieldType\HistoryStep $present */
186    $present = $this->get('present')->first();
187    $tree = new SourceTree($present->getData() ?? []);
188    $indexed = $tree->getTree();
189    $hash = self::getUniqId($indexed);
190
191    $this->set('present', [
192      'data' => $indexed,
193      'hash' => $hash,
194      'log' => $present->getLog(),
195      'time' => $present->getTime(),
196      'user' => $present->getUser(),
197    ]);
198  }
199
200  /**
201   * {@inheritdoc}
202   */
203  public function getProfile(): ?ProfileInterface {
204    $profile_id = $this->get('profileId')->getString();
205    /** @var \Drupal\display_builder\ProfileInterface $profile */
206    $profile = $this->entityTypeManager()->getStorage('display_builder_profile')->load($profile_id);
207
208    return $profile;
209  }
210
211  /**
212   * {@inheritdoc}
213   */
214  public function setProfile(string $profile_id): void {
215    $this->set('profileId', $profile_id);
216  }
217
218  /**
219   * {@inheritdoc}
220   */
221  public function moveToRoot(string $node_id, int $position): bool {
222    $tree = new SourceTree($this->getCurrentState());
223    $data = $tree->getNodeData($node_id);
224
225    if (!$data) {
226      return FALSE;
227    }
228
229    if (!$tree->moveToRoot($node_id, $position)) {
230      return FALSE;
231    }
232
233    // Get friendly label to display in log instead of ids.
234    $labelWithSummary = $this->slotSourceProxy()->getLabelWithSummary($data, $this->getContexts());
235
236    $log = new FormattableMarkup('%node @thingy has been moved to root', [
237      '%node' => $labelWithSummary['summary'],
238      '@thingy' => $data['source_id'],
239    ]);
240    $this->setNewPresent($tree->getTree(), $log, TRUE, FALSE);
241
242    return TRUE;
243  }
244
245  /**
246   * {@inheritdoc}
247   */
248  public function moveToSlot(string $node_id, string $parent_id, string $slot_id, int $position): bool {
249    $tree = new SourceTree($this->getCurrentState());
250    $data = $tree->getNodeData($node_id);
251
252    if (!$data) {
253      return FALSE;
254    }
255
256    if (!$tree->moveToSlot($node_id, $parent_id, $slot_id, $position)) {
257      return FALSE;
258    }
259
260    // Get friendly label to display in log instead of ids.
261    $labelWithSummary = $this->slotSourceProxy()->getLabelWithSummary($data, $this->getContexts());
262    $labelWithSummaryParent = $this->slotSourceProxy()->getLabelWithSummary($tree->getNodeData($parent_id));
263
264    $log = new FormattableMarkup("%node @thingy has been moved to %parent's @slot_id", [
265      '%node' => $labelWithSummary['summary'],
266      '@thingy' => $data['source_id'],
267      '%parent' => $labelWithSummaryParent['summary'],
268      '@slot_id' => $slot_id,
269    ]);
270
271    $this->setNewPresent($tree->getTree(), $log, TRUE, FALSE);
272
273    return TRUE;
274  }
275
276  /**
277   * {@inheritdoc}
278   */
279  public function attachToRoot(int $position, string $source_id, array $data, array $third_party_settings = []): string {
280    $tree = new SourceTree($this->getCurrentState());
281    $node_id = $tree->attachToRoot($position, $source_id, $data);
282
283    if ($third_party_settings) {
284      foreach ($third_party_settings as $island_id => $settings) {
285        $tree->setThirdPartySettings($node_id, $island_id, $settings);
286      }
287    }
288
289    $new_data = $tree->getNode($node_id);
290
291    // Get friendly label to display in log instead of ids.
292    $labelWithSummary = $this->slotSourceProxy()->getLabelWithSummary($new_data, $this->getContexts() ?? []);
293
294    $log = new FormattableMarkup('%node @source_id has been attached to root', [
295      '%node' => $labelWithSummary['summary'],
296      '@source_id' => $source_id,
297    ]);
298    $this->setNewPresent($tree->getTree(), $log, FALSE, FALSE);
299
300    return $node_id;
301  }
302
303  /**
304   * {@inheritdoc}
305   */
306  public function attachToSlot(string $parent_id, string $slot_id, int $position, string $source_id, array $data, array $third_party_settings = []): string {
307    $tree = new SourceTree($this->getCurrentState());
308    $node_id = $tree->attachToSlot($parent_id, $slot_id, $position, $source_id, $data);
309
310    if (!$node_id) {
311      throw new \Exception('Parent or slot not found');
312    }
313
314    if ($third_party_settings) {
315      foreach ($third_party_settings as $island_id => $settings) {
316        $tree->setThirdPartySettings($node_id, $island_id, $settings);
317      }
318    }
319
320    $new_data = $tree->getNode($node_id);
321
322    // Get friendly label to display in log instead of ids.
323    $labelWithSummary = $this->slotSourceProxy()->getLabelWithSummary($new_data, $this->getContexts() ?? []);
324    $labelWithSummaryParent = $this->slotSourceProxy()->getLabelWithSummary($tree->getNode($parent_id));
325
326    $log = new FormattableMarkup("%node @source_id has been attached to %parent's @slot_id", [
327      '%node' => $labelWithSummary['summary'],
328      '@source_id' => $source_id,
329      '%parent' => $labelWithSummaryParent['summary'],
330      '@slot_id' => $slot_id,
331    ]);
332    $this->setNewPresent($tree->getTree(), $log, TRUE, FALSE);
333
334    return $node_id;
335  }
336
337  /**
338   * {@inheritdoc}
339   */
340  public function getNode(string $node_id): array {
341    $root = $this->getCurrentState();
342    $path = $this->getPath($node_id);
343    $value = NestedArray::getValue($root, $path);
344
345    return $value ?? [];
346  }
347
348  /**
349   * {@inheritdoc}
350   */
351  public function getParentId(string $node_id): string {
352    return $this->getPathIndex()[$node_id]['parent'] ?? '';
353  }
354
355  /**
356   * {@inheritdoc}
357   */
358  public function setSource(string $node_id, string $source_id, array $data): void {
359    $tree = new SourceTree($this->getCurrentState());
360
361    if (!$tree->setSource($node_id, $source_id, $data)) {
362      throw new \Exception('Node ID mismatch');
363    }
364
365    // Get friendly label to display in log instead of ids.
366    $labelWithSummary = $this->slotSourceProxy()->getLabelWithSummary($tree->getNodeData($node_id), $this->getContexts());
367
368    $log = new FormattableMarkup('%source has been updated', [
369      '%source' => $labelWithSummary['summary'],
370    ]);
371    $this->setNewPresent($tree->getTree(), $log, TRUE, FALSE);
372  }
373
374  /**
375   * {@inheritdoc}
376   */
377  public function setThirdPartySettings(string $node_id, string $island_id, array $data): void {
378    $tree = new SourceTree($this->getCurrentState());
379
380    if (!$tree->setThirdPartySettings($node_id, $island_id, $data)) {
381      return;
382    }
383
384    // Get friendly label to display in log instead of ids.
385    $labelWithSummary = $this->slotSourceProxy()->getLabelWithSummary($tree->getNodeData($node_id), $this->getContexts());
386
387    $log = new FormattableMarkup('%source has been updated by @island_id', [
388      '%source' => $labelWithSummary['summary'],
389      '@island_id' => $island_id,
390    ]);
391    $this->setNewPresent($tree->getTree(), $log, TRUE, FALSE);
392  }
393
394  /**
395   * {@inheritdoc}
396   */
397  public function remove(string $node_id): void {
398    $tree = new SourceTree($this->getCurrentState());
399    $data = $tree->getNodeData($node_id);
400
401    if (!$data) {
402      return;
403    }
404    $parent_id = $tree->getParentId($node_id);
405
406    $contexts = $this->getContexts() ?? [];
407
408    // Get friendly label to display in log instead of ids.
409    $labelWithSummary = $this->slotSourceProxy()->getLabelWithSummary($data, $contexts);
410    $labelWithSummaryParent = empty($parent_id) ? ['summary' => 'root'] : $this->slotSourceProxy()->getLabelWithSummary($tree->getNodeData($parent_id), $contexts);
411
412    $tree->remove($node_id);
413
414    $log = new FormattableMarkup('%node has been removed from %parent', [
415      '%node' => $labelWithSummary['summary'],
416      '%parent' => $labelWithSummaryParent['summary'],
417    ]);
418    $this->setNewPresent($tree->getTree(), $log, FALSE, FALSE);
419  }
420
421  /**
422   * {@inheritdoc}
423   */
424  public function getContexts(): ?array {
425    if ($this->get('contexts')->isEmpty()) {
426      return [];
427    }
428
429    return $this->refreshContexts($this->get('contexts')->first()->getValue());
430  }
431
432  /**
433   * {@inheritdoc}
434   */
435  public function setSave(array $save_data): void {
436    $tree = new SourceTree($save_data);
437    $indexed = $tree->getTree();
438    $hash = self::getUniqId($indexed);
439    $this->set('save', ['data' => $indexed, 'hash' => $hash, 'log' => NULL, 'time' => \time(), 'user' => NULL]);
440  }
441
442  /**
443   * {@inheritdoc}
444   *
445   * @see \Drupal\display_builder\HistoryInterface
446   */
447  public function getCurrentState(): array {
448    return $this->getCurrent()?->getData() ?? [];
449  }
450
451  /**
452   * {@inheritdoc}
453   */
454  public function restore(): void {
455    /** @var \Drupal\display_builder\Plugin\Field\FieldType\HistoryStep|null $first */
456    $first = $this->get('save')->first();
457    $this->setNewPresent($first->getData(), 'Back to saved data.');
458  }
459
460  /**
461   * {@inheritdoc}
462   *
463   * @see \Drupal\display_builder\HistoryInterface
464   */
465  public function undo(): void {
466    $past = $this->get('past');
467
468    if ($past->isEmpty()) {
469      return;
470    }
471
472    $present_values = $this->get('present')->getValue();
473    \assert(\array_is_list($present_values));
474
475    // Remove the last element from the past.
476    $past_values = $past->getValue();
477    \assert(\array_is_list($past_values));
478    $last = \array_pop($past_values);
479    \assert(!\array_is_list($last));
480    $this->set('past', $past_values);
481
482    // Set the present to the element we removed in the previous step.
483    $this->set('present', $last);
484    // Insert the old present state at the beginning of the future.
485    $this->set('future', \array_merge($present_values, $this->get('future')->getValue()));
486  }
487
488  /**
489   * {@inheritdoc}
490   *
491   * @see \Drupal\display_builder\HistoryInterface
492   */
493  public function redo(): void {
494    $future = $this->get('future');
495
496    if ($future->isEmpty()) {
497      return;
498    }
499
500    // Remove the first element from the future.
501    $first = $future->first()->getValue();
502    \assert(!\array_is_list($first));
503    $future->removeItem(0);
504    // Insert the old present state at the end of the past.
505    $this->get('past')->appendItem($this->get('present')->first());
506    // Set the present to the element we removed in the previous step.
507    $this->set('present', $first);
508    $this->set('future', $future->getValue());
509  }
510
511  /**
512   * {@inheritdoc}
513   */
514  public function clear(): void {
515    $this->set('past', NULL);
516    $this->set('future', NULL);
517  }
518
519  /**
520   * {@inheritdoc}
521   */
522  public function isHistoryNew(): bool {
523    return $this->present === NULL && empty($this->past) && empty($this->future);
524  }
525
526  /**
527   * {@inheritdoc}
528   *
529   * @see \Drupal\display_builder\HistoryInterface
530   */
531  public function getCountPast(): int {
532    return $this->get('past')->count();
533  }
534
535  /**
536   * {@inheritdoc}
537   *
538   * @see \Drupal\display_builder\HistoryInterface
539   */
540  public function getCountFuture(): int {
541    return $this->get('future')->count();
542  }
543
544  /**
545   * {@inheritdoc}
546   */
547  public function getUsers(): array {
548    $users = [];
549    $steps = \array_merge($this->get('past')->getValue(), $this->get('present')->getValue(), $this->get('future')->getValue());
550
551    foreach ($steps as $step) {
552      if ($step === NULL) {
553        continue;
554      }
555      $user_id = $step['user'];
556
557      if ($user_id !== NULL && (!isset($users[$user_id]) || $step['time'] > $users[$user_id])) {
558        $users[$user_id] = $step['time'];
559      }
560    }
561
562    return $users;
563  }
564
565  /**
566   * {@inheritdoc}
567   */
568  public function canSaveContextsRequirement(?array $contexts = NULL): bool {
569    $contexts ??= $this->getContexts();
570
571    if ($contexts === NULL) {
572      return FALSE;
573    }
574
575    if (!\array_key_exists('context_requirements', $contexts)
576      || !($contexts['context_requirements'] instanceof RequirementsContext)) {
577      return FALSE;
578    }
579
580    return TRUE;
581  }
582
583  /**
584   * {@inheritdoc}
585   */
586  public function hasSaveContextsRequirement(string $key, array $contexts = []): bool {
587    $contexts = empty($contexts) ? $this->getContexts() : $contexts;
588    // Some strange edge cases where context is null.
589    $contexts ??= [];
590
591    if (!\array_key_exists('context_requirements', $contexts)
592      || !($contexts['context_requirements'] instanceof RequirementsContext)
593      || !$contexts['context_requirements']->hasValue($key)) {
594      return FALSE;
595    }
596
597    return TRUE;
598  }
599
600  /**
601   * {@inheritdoc}
602   */
603  public function hasSave(): bool {
604    return !$this->get('save')->isEmpty();
605  }
606
607  /**
608   * {@inheritdoc}
609   */
610  public function saveIsCurrent(): bool {
611    $present = $this->get('present');
612    $save = $this->get('save');
613
614    // If either present or save is null, they can't be equal unless both are
615    // null.
616    if ($present->isEmpty() || $save->isEmpty()) {
617      return $present->isEmpty() && $save->isEmpty();
618    }
619
620    /** @var \Drupal\display_builder\Plugin\Field\FieldType\HistoryStep|null $present */
621    $present = $present->first();
622    /** @var \Drupal\display_builder\Plugin\Field\FieldType\HistoryStep|null $save */
623    $save = $save->first();
624
625    return $present->getHash() === $save->getHash();
626  }
627
628  /**
629   * {@inheritdoc}
630   */
631  public function getPathIndex(): array {
632    $tree = new SourceTree($this->getCurrentState());
633
634    return $tree->getPathIndex();
635  }
636
637  /**
638   * {@inheritdoc}
639   *
640   * @see \Drupal\display_builder\HistoryInterface
641   */
642  public function setNewPresent(array $data, FormattableMarkup|string $log_message = '', bool $check_hash = TRUE, bool $index = TRUE): void {
643    if ($index) {
644      $tree = new SourceTree($data);
645      $data = $tree->getTree();
646    }
647    $hash = self::getUniqId($data);
648
649    if (!$this->get('present')->isEmpty()) {
650      /** @var \Drupal\display_builder\Plugin\Field\FieldType\HistoryStep|null $present */
651      $present = $this->get('present')->first();
652
653      // Check if this present is the same to avoid duplicates, for example move
654      // to the same place.
655      if ($check_hash && $hash === $present->getHash()) {
656        return;
657      }
658
659      // 1. Insert the present at the end of the past.
660      // If it's the very first action, we want a NULL in the past to be able to
661      // undo to initial empty state.
662      $this->get('past')->appendItem($present->getValue());
663    }
664
665    // Keep only the last x history.
666    if ($this->get('past')->count() > self::MAX_HISTORY) {
667      $this->get('past')->removeItem(0);
668    }
669
670    // 2. Set the present to the new state.
671    $this->set('present', [
672      'data' => $data,
673      'hash' => $hash,
674      'log' => $log_message,
675      'time' => \time(),
676      'user' => (int) $this->currentUser()->id(),
677    ]);
678
679    // 3. Clear the future.
680    $this->set('future', []);
681  }
682
683  /**
684   * {@inheritdoc}
685   *
686   * @see \Drupal\display_builder\HistoryInterface
687   */
688  public function getCurrent(): ?HistoryStep {
689    /** @var \Drupal\display_builder\Plugin\Field\FieldType\HistoryStep|null $step */
690    $step = $this->get('present')->first();
691
692    return $step;
693  }
694
695  /**
696   * {@inheritdoc}
697   */
698  public static function getUniqId(array $data): int {
699    return \crc32((string) \serialize($data));
700  }
701
702  /**
703   * Sample entity generator.
704   */
705  private function sampleEntityGenerator(): SampleEntityGeneratorInterface {
706    return $this->sampleEntityGenerator ??= \Drupal::service('ui_patterns.sample_entity_generator');
707  }
708
709  /**
710   * Slot source proxy.
711   */
712  private function slotSourceProxy(): SlotSourceProxy {
713    return $this->slotSourceProxy ??= \Drupal::service('display_builder.slot_sources_proxy');
714  }
715
716  /**
717   * Slot source proxy.
718   */
719  private function currentUser(): AccountInterface {
720    return $this->currentUser ??= \Drupal::service('current_user');
721  }
722
723  /**
724   * Refresh contexts after loaded from storage.
725   *
726   * @param \Drupal\Core\Plugin\Context\ContextInterface[] $contexts
727   *   The contexts.
728   *
729   * @throws \Drupal\Component\Plugin\Exception\ContextException
730   * @throws \Drupal\Component\Plugin\Exception\InvalidPluginDefinitionException
731   * @throws \Drupal\Component\Plugin\Exception\PluginNotFoundException
732   *
733   * @return array
734   *   The refreshed contexts or NULL if no context.
735   */
736  private function refreshContexts(array $contexts): array {
737    foreach ($contexts as &$context) {
738      if ($context instanceof EntityContext) {
739        // @todo We should use cache entries here
740        // with the corresponding cache contexts in it.
741        // This may avoid some unnecessary entity loads or generation.
742        $entity = $context->getContextValue();
743
744        // Check if sample entity.
745        if ($entity->id()) {
746          $entity = $this->entityTypeManager()->getStorage($entity->getEntityTypeId())->load($entity->id());
747        }
748        else {
749          $entity = $this->sampleEntityGenerator()->get($entity->getEntityTypeId(), $entity->bundle());
750        }
751
752        // Edge case when the parent entity is deleted but not the builder
753        // instance.
754        if (!$entity) {
755          return $contexts;
756        }
757        $context = (\get_class($context))::fromEntity($entity);
758      }
759    }
760
761    return $contexts;
762  }
763
764  /**
765   * Get the path to an source.
766   *
767   * @param string $node_id
768   *   The node id of the source.
769   *
770   * @return array
771   *   The path, one array item by level.
772   */
773  private function getPath(string $node_id): array {
774    return $this->getPathIndex()[$node_id]['path'] ?? [];
775  }
776
777}