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

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.

Instance->attachToRoot
262  public function attachToRoot(int $position, string $source_id, array $data, array $third_party_settings = []): string {
263    $data = [
264      'node_id' => \uniqid(),
265      'source_id' => $source_id,
266      'source' => $data,
267    ];
268
269    if ($third_party_settings) {
270      $data['_third_party_settings'] = $third_party_settings;
271    }
272
273    $root = $this->getCurrentState();
273    $root = $this->getCurrentState();
274    $root = $this->doAttachToRoot($root, $position, $data);
275
276    // Get friendly label to display in log instead of ids.
277    $labelWithSummary = $this->slotSourceProxy()->getLabelWithSummary($data, $this->getContexts() ?? []);
278
279    $log = new FormattableMarkup('%node @source_id has been attached to root', [
280      '%node' => $labelWithSummary['summary'],
281      '@source_id' => $source_id,
282    ]);
283    $this->setNewPresent($root, $log, FALSE);
284
285    return $data['node_id'];
Instance->attachToSlot
291  public function attachToSlot(string $parent_id, string $slot_id, int $position, string $source_id, array $data, array $third_party_settings = []): string {
292    $root = $this->getCurrentState();
293    $data = [
294      'node_id' => \uniqid(),
295      'source_id' => $source_id,
296      'source' => $data,
297    ];
298
299    if ($third_party_settings) {
300      $data['_third_party_settings'] = $third_party_settings;
301    }
302
303    $root = $this->doAttachToSlot($root, $parent_id, $slot_id, $position, $data);
303    $root = $this->doAttachToSlot($root, $parent_id, $slot_id, $position, $data);
304
305    // Get friendly label to display in log instead of ids.
306    $labelWithSummary = $this->slotSourceProxy()->getLabelWithSummary($data, $this->getContexts() ?? []);
307    $labelWithSummaryParent = $this->slotSourceProxy()->getLabelWithSummary($this->get($parent_id));
308
309    $log = new FormattableMarkup("%node @source_id has been attached to %parent's @slot_id", [
310      '%node' => $labelWithSummary['summary'],
311      '@source_id' => $source_id,
312      '%parent' => $labelWithSummaryParent['summary'],
313      '@slot_id' => $slot_id,
314    ]);
315    $this->setNewPresent($root, $log);
316
317    return $data['node_id'];
Instance->buildIndexFromSlot
662  private function buildIndexFromSlot(array $path, array $data = []): array {
663    foreach ($data as $index => $source) {
663    foreach ($data as $index => $source) {
663    foreach ($data as $index => $source) {
663    foreach ($data as $index => $source) {
664      $source_path = \array_merge($path, [$index]);
665      $data[$index] = $this->buildIndexFromSource($source_path, $source);
666    }
667
668    return $data;
Instance->buildIndexFromSource
790  private function buildIndexFromSource(array $path, array $data = []): array {
791    // First job: Add missing node_id keys.
792    $node_id = $data['node_id'] ?? \uniqid();
793    $data['node_id'] = $node_id;
794    // Second job: Save the path to the index.
795    $this->pathIndex[$node_id] = $path;
796
797    if (!isset($data['source_id'])) {
798      return $data;
802    if ($data['source_id'] !== 'component') {
803      return $data;
806    if (!isset($data['source']['component']['slots'])) {
807      return $data;
810    foreach ($data['source']['component']['slots'] as $slot_id => $slot) {
810    foreach ($data['source']['component']['slots'] as $slot_id => $slot) {
810    foreach ($data['source']['component']['slots'] as $slot_id => $slot) {
811      if (!isset($slot['sources'])) {
812        continue;
810    foreach ($data['source']['component']['slots'] as $slot_id => $slot) {
811      if (!isset($slot['sources'])) {
812        continue;
813      }
814      $slot_path = \array_merge($path, ['source', 'component', 'slots', $slot_id, 'sources']);
810    foreach ($data['source']['component']['slots'] as $slot_id => $slot) {
811      if (!isset($slot['sources'])) {
812        continue;
813      }
814      $slot_path = \array_merge($path, ['source', 'component', 'slots', $slot_id, 'sources']);
815      $slot['sources'] = $this->buildIndexFromSlot($slot_path, $slot['sources']);
816      $data['source']['component']['slots'][$slot_id] = $slot;
817    }
818
819    return $data;
Instance->canSaveContextsRequirement
536  public function canSaveContextsRequirement(?array $contexts = NULL): bool {
537    $contexts ??= $this->getContexts();
538
539    if ($contexts === NULL) {
540      return FALSE;
543    if (!\array_key_exists('context_requirements', $contexts)
544      || !($contexts['context_requirements'] instanceof RequirementsContext)) {
545      return FALSE;
548    return TRUE;
Instance->changeSourcePositionInSlot
746  private function changeSourcePositionInSlot(array $slot, string $node_id, int $to): array {
747    foreach ($slot as $position => $source) {
747    foreach ($slot as $position => $source) {
747    foreach ($slot as $position => $source) {
748      if ($source['node_id'] === $node_id) {
749        $p1 = \array_splice($slot, $position, 1);
750        $p2 = \array_splice($slot, 0, $to);
751
752        return \array_merge($p2, $p1, $slot);
747    foreach ($slot as $position => $source) {
748      if ($source['node_id'] === $node_id) {
749        $p1 = \array_splice($slot, $position, 1);
750        $p2 = \array_splice($slot, 0, $to);
751
752        return \array_merge($p2, $p1, $slot);
753      }
754    }
755
756    return $slot;
Instance->clear
493    $this->past = [];
494    $this->future = [];
495  }
Instance->currentUser
689    return $this->currentUser ??= \Drupal::service('current_user');
Instance->doAttachToRoot
835  private function doAttachToRoot(array $root, int $position, array $data): array {
836    \array_splice($root, $position, 0, [$data]);
837
838    return $root;
Instance->doAttachToSlot
858  private function doAttachToSlot(array $root, string $parent_id, string $slot_id, int $position, array $data): array {
859    $parent_path = $this->getPath($root, $parent_id);
860    $slot_path = \array_merge($parent_path, ['source', 'component', 'slots', $slot_id, 'sources']);
861    $slot = NestedArray::getValue($root, $slot_path) ?? [];
862    \array_splice($slot, $position, 0, [$data]);
863    NestedArray::setValue($root, $slot_path, $slot);
864
865    return $root;
Instance->doRemove
879  private function doRemove(array $root, string $node_id): array {
880    $path = $this->getPath($root, $node_id);
881    NestedArray::unsetValue($root, $path);
882    // To avoid non consecutive array keys, we rebuild the value list.
883    $slot_path = \array_slice($path, 0, \count($path) - 1);
884    $slot = NestedArray::getValue($root, $slot_path);
885    NestedArray::setValue($root, $slot_path, \array_values($slot));
886
887    return $root;
Instance->get
323  public function get(string $node_id): array {
324    $root = $this->getCurrentState();
325    $path = $this->getPath($root, $node_id);
326    $value = NestedArray::getValue($root, $path);
327
328    return $value ?? [];
Instance->getContexts
417    return $this->refreshContexts($this->contexts);
Instance->getCountFuture
512    return \count($this->future);
Instance->getCountPast
503    return \count($this->past);
Instance->getCurrent
641    return $this->present;
Instance->getCurrentState
434    return $this->getCurrent()->data ?? [];
Instance->getNodeId
767  private function getNodeId(array $path): string {
768    $index = $this->getPathIndex();
769
770    foreach ($index as $node_id => $node_path) {
770    foreach ($index as $node_id => $node_path) {
770    foreach ($index as $node_id => $node_path) {
771      if ($path === $node_path) {
772        return $node_id;
770    foreach ($index as $node_id => $node_path) {
771      if ($path === $node_path) {
772        return $node_id;
773      }
774    }
775
776    return '';
Instance->getParentId
334  public function getParentId(array $root, string $node_id): string {
335    $path = $this->getPath($root, $node_id);
336    $length = \count(['source', 'component', 'slots', '{slot_id}', 'sources', '{position}']);
337    $parent_path = \array_slice($path, 0, \count($path) - $length);
338
339    return $this->getNodeId($parent_path);
Instance->getPath
901  private function getPath(array $root, string $node_id): array {
902    return $this->getPathIndex($root)[$node_id] ?? [];
Instance->getPathIndex
585  public function getPathIndex(array $root = []): array {
586    if (empty($root)) {
590      $root = $this->getCurrentState();
591    }
592    // It may be slow to rebuild the index every time we request it. But it is
593    // very difficult to maintain an index synchronized with the state storage
594    // history.
595    $this->buildIndexFromSlot([], $root);
595    $this->buildIndexFromSlot([], $root);
596
597    return $this->pathIndex ?? [];
Instance->getProfile
175    $profile = $this->entityTypeManager()->getStorage('display_builder_profile')->load($this->profileId);
176
177    return $profile;
Instance->getUniqId
647  public static function getUniqId(array $data): int {
648    return \crc32((string) \serialize($data));
Instance->getUsers
519    $users = [];
520    $steps = \array_merge($this->past, [$this->present], $this->future);
521
522    foreach ($steps as $step) {
522    foreach ($steps as $step) {
523      $user_id = $step->user ?? NULL;
524
525      if ($user_id && ($users[$user_id] ?? $step->time > 0)) {
525      if ($user_id && ($users[$user_id] ?? $step->time > 0)) {
522    foreach ($steps as $step) {
523      $user_id = $step->user ?? NULL;
524
525      if ($user_id && ($users[$user_id] ?? $step->time > 0)) {
526        $users[$user_id] = $step->time;
522    foreach ($steps as $step) {
523      $user_id = $step->user ?? NULL;
524
525      if ($user_id && ($users[$user_id] ?? $step->time > 0)) {
526        $users[$user_id] = $step->time;
527      }
528    }
529
530    return $users;
Instance->hasSave
572    return !empty($this->save);
Instance->hasSaveContextsRequirement
554  public function hasSaveContextsRequirement(string $key, array $contexts = []): bool {
555    $contexts = empty($contexts) ? $this->getContexts() : $contexts;
555    $contexts = empty($contexts) ? $this->getContexts() : $contexts;
555    $contexts = empty($contexts) ? $this->getContexts() : $contexts;
555    $contexts = empty($contexts) ? $this->getContexts() : $contexts;
556    // Some strange edge cases where context is null.
557    $contexts ??= [];
558
559    if (!\array_key_exists('context_requirements', $contexts)
560      || !($contexts['context_requirements'] instanceof RequirementsContext)
561      || !$contexts['context_requirements']->hasValue($key)) {
562      return FALSE;
565    return TRUE;
Instance->isNew
139    return !$this->id();
Instance->moveToRoot
190  public function moveToRoot(string $node_id, int $position): bool {
191    $root = $this->getCurrentState();
192    $path = $this->getPath($root, $node_id);
193    $data = NestedArray::getValue($root, $path);
194
195    if (empty($data) || !isset($data['source_id'])) {
195    if (empty($data) || !isset($data['source_id'])) {
196      return FALSE;
199    $root = $this->doRemove($root, $node_id);
200    $root = $this->doAttachToRoot($root, $position, $data);
201
202    // Get friendly label to display in log instead of ids.
203    $labelWithSummary = $this->slotSourceProxy()->getLabelWithSummary($data, $this->getContexts());
204
205    $log = new FormattableMarkup('%node @thingy has been moved to root', [
206      '%node' => $labelWithSummary['summary'],
207      '@thingy' => $data['source_id'],
208    ]);
209    $this->setNewPresent($root, $log);
210
211    return TRUE;
Instance->moveToSlot
217  public function moveToSlot(string $node_id, string $parent_id, string $slot_id, int $position): bool {
218    $root = $this->getCurrentState();
219    $path = $this->getPath($root, $node_id);
220    $data = NestedArray::getValue($root, $path);
221
222    if (empty($data) || !isset($data['source_id'])) {
222    if (empty($data) || !isset($data['source_id'])) {
223      return FALSE;
226    $parent_slot = \array_slice($path, \count($path) - 3, 1)[0];
227
228    if (($parent_id === $this->getParentId($root, $node_id)) && ($slot_id === $parent_slot)) {
228    if (($parent_id === $this->getParentId($root, $node_id)) && ($slot_id === $parent_slot)) {
228    if (($parent_id === $this->getParentId($root, $node_id)) && ($slot_id === $parent_slot)) {
229      // Moving to the same slot is tricky, because we don't want to remove a
230      // sibling.
231      $slot_path = \array_slice($path, 0, \count($path) - 1);
239      $root = $this->doRemove($root, $node_id);
240      $root = $this->doAttachToSlot($root, $parent_id, $slot_id, $position, $data);
241    }
242
243    // Get friendly label to display in log instead of ids.
244    $labelWithSummary = $this->slotSourceProxy()->getLabelWithSummary($data, $this->getContexts());
244    $labelWithSummary = $this->slotSourceProxy()->getLabelWithSummary($data, $this->getContexts());
245    $labelWithSummaryParent = $this->slotSourceProxy()->getLabelWithSummary($this->get($parent_id));
246
247    $log = new FormattableMarkup("%node @thingy has been moved to %parent's @slot_id", [
248      '%node' => $labelWithSummary['summary'],
249      '@thingy' => $data['source_id'],
250      '%parent' => $labelWithSummaryParent['summary'],
251      '@slot_id' => $slot_id,
252    ]);
253
254    $this->setNewPresent($root, $log);
255
256    return TRUE;
Instance->postCreate
164  public function postCreate(EntityStorageInterface $storage): void {
165    if ($this->present) {
166      $this->present->data = $this->buildIndexFromSlot([], $this->getCurrentState());
167    }
168  }
168  }
Instance->redo
472    $future = $this->future ?? [];
473
474    if (empty($future)) {
475      return;
479    $first = \array_shift($future);
480    // Insert the old present state at the end of the past.
481    $this->past = \array_merge($this->past, [$this->present]);
482    // Set the present to the element we removed in the previous step.
483    $this->present = $first;
484    $this->future = $future;
485  }
Instance->refreshContexts
705  private function refreshContexts(array $contexts): array {
706    foreach ($contexts as &$context) {
706    foreach ($contexts as &$context) {
707      if ($context instanceof EntityContext) {
711        $entity = $context->getContextValue();
712
713        // Check if sample entity.
714        if ($entity->id()) {
714        if ($entity->id()) {
715          $entity = $this->entityTypeManager()->getStorage($entity->getEntityTypeId())->load($entity->id());
718          $entity = $this->sampleEntityGenerator()->get($entity->getEntityTypeId(), $entity->bundle());
719        }
720
721        // Edge case when the parent entity is deleted but not the builder
722        // instance.
723        if (!$entity) {
723        if (!$entity) {
724          return $contexts;
706    foreach ($contexts as &$context) {
707      if ($context instanceof EntityContext) {
708        // @todo We should use cache entries here
709        // with the corresponding cache contexts in it.
710        // This may avoid some unnecessary entity loads or generation.
711        $entity = $context->getContextValue();
712
713        // Check if sample entity.
714        if ($entity->id()) {
715          $entity = $this->entityTypeManager()->getStorage($entity->getEntityTypeId())->load($entity->id());
716        }
717        else {
718          $entity = $this->sampleEntityGenerator()->get($entity->getEntityTypeId(), $entity->bundle());
719        }
720
721        // Edge case when the parent entity is deleted but not the builder
722        // instance.
723        if (!$entity) {
724          return $contexts;
725        }
726        $context = (\get_class($context))::fromEntity($entity);
706    foreach ($contexts as &$context) {
707      if ($context instanceof EntityContext) {
708        // @todo We should use cache entries here
709        // with the corresponding cache contexts in it.
710        // This may avoid some unnecessary entity loads or generation.
711        $entity = $context->getContextValue();
712
713        // Check if sample entity.
714        if ($entity->id()) {
715          $entity = $this->entityTypeManager()->getStorage($entity->getEntityTypeId())->load($entity->id());
716        }
717        else {
718          $entity = $this->sampleEntityGenerator()->get($entity->getEntityTypeId(), $entity->bundle());
719        }
720
721        // Edge case when the parent entity is deleted but not the builder
722        // instance.
723        if (!$entity) {
724          return $contexts;
725        }
726        $context = (\get_class($context))::fromEntity($entity);
727      }
728    }
729
730    return $contexts;
Instance->remove
393  public function remove(string $node_id): void {
394    $root = $this->getCurrentState();
395    $path = $this->getPath($root, $node_id);
396    $data = NestedArray::getValue($root, $path);
397    $parent_id = $this->getParentId($root, $node_id);
398    $root = $this->doRemove($root, $node_id);
399
400    $contexts = $this->getContexts() ?? [];
401
402    // Get friendly label to display in log instead of ids.
403    $labelWithSummary = $this->slotSourceProxy()->getLabelWithSummary($data, $contexts);
404    $labelWithSummaryParent = empty($parent_id) ? ['summary' => 'root'] : $this->slotSourceProxy()->getLabelWithSummary($this->get($parent_id), $contexts);
404    $labelWithSummaryParent = empty($parent_id) ? ['summary' => 'root'] : $this->slotSourceProxy()->getLabelWithSummary($this->get($parent_id), $contexts);
404    $labelWithSummaryParent = empty($parent_id) ? ['summary' => 'root'] : $this->slotSourceProxy()->getLabelWithSummary($this->get($parent_id), $contexts);
404    $labelWithSummaryParent = empty($parent_id) ? ['summary' => 'root'] : $this->slotSourceProxy()->getLabelWithSummary($this->get($parent_id), $contexts);
405
406    $log = new FormattableMarkup('%node has been removed from %parent', [
407      '%node' => $labelWithSummary['summary'],
408      '%parent' => $labelWithSummaryParent['summary'],
409    ]);
410    $this->setNewPresent($root, $log, FALSE);
411  }
Instance->restore
441    $this->setNewPresent($this->save->data, 'Back to saved data.');
442  }
Instance->sampleEntityGenerator
675    return $this->sampleEntityGenerator ??= \Drupal::service('ui_patterns.sample_entity_generator');
Instance->saveIsCurrent
579    return $this->present->hash === $this->save->hash;
Instance->setNewPresent
605  public function setNewPresent(array $data, FormattableMarkup|string $log_message = '', bool $check_hash = TRUE): void {
606    $hash = self::getUniqId($data);
607
608    // Check if this present is the same to avoid duplicates, for example move
609    // to the same place.
610    if ($check_hash && $hash === $this->present?->hash) {
610    if ($check_hash && $hash === $this->present?->hash) {
611      return;
615    $this->past[] = $this->present;
616
617    // Keep only the last x history.
618    if (\count($this->past) > self::MAX_HISTORY) {
619      \array_shift($this->past);
620    }
621
622    // 2. Set the present to the new state.
623    $this->present = new HistoryStep(
623    $this->present = new HistoryStep(
624      $data,
625      $hash,
626      $log_message,
627      \time(),
628      (int) $this->currentUser()->id(),
629    );
630
631    // 3. Clear the future.
632    $this->future = [];
633  }
Instance->setProfile
183  public function setProfile(string $profile_id): void {
184    $this->profileId = $profile_id;
185  }
Instance->setSave
423  public function setSave(array $save_data): void {
424    $hash = self::getUniqId($save_data);
425    $this->save = new HistoryStep($save_data, $hash, NULL, \time(), NULL);
426  }
Instance->setSource
345  public function setSource(string $node_id, string $source_id, array $data): void {
346    $root = $this->getCurrentState();
347    $path = $this->getPath($root, $node_id);
348    $existing_data = NestedArray::getValue($root, $path) ?? [];
349
350    if (!isset($existing_data['node_id']) || ($existing_data['node_id'] !== $node_id)) {
350    if (!isset($existing_data['node_id']) || ($existing_data['node_id'] !== $node_id)) {
351      throw new \Exception('Node ID mismatch');
353    $existing_data['source_id'] = $source_id;
354    $existing_data['source'] = $data;
355    NestedArray::setValue($root, $path, $existing_data);
356
357    // Get friendly label to display in log instead of ids.
358    $labelWithSummary = $this->slotSourceProxy()->getLabelWithSummary($existing_data, $this->getContexts());
359
360    $log = new FormattableMarkup('%source has been updated', [
361      '%source' => $labelWithSummary['summary'],
362    ]);
363    $this->setNewPresent($root, $log);
364  }
Instance->setThirdPartySettings
369  public function setThirdPartySettings(string $node_id, string $island_id, array $data): void {
370    $root = $this->getCurrentState();
371    $path = $this->getPath($root, $node_id);
372    $existing_data = NestedArray::getValue($root, $path);
373
374    if (!isset($existing_data['_third_party_settings'])) {
375      $existing_data['_third_party_settings'] = [];
376    }
377    $existing_data['_third_party_settings'][$island_id] = $data;
377    $existing_data['_third_party_settings'][$island_id] = $data;
378    NestedArray::setValue($root, $path, $existing_data);
379
380    // Get friendly label to display in log instead of ids.
381    $labelWithSummary = $this->slotSourceProxy()->getLabelWithSummary($existing_data, $this->getContexts());
382
383    $log = new FormattableMarkup('%source has been updated by @island_id', [
384      '%source' => $labelWithSummary['summary'],
385      '@island_id' => $island_id,
386    ]);
387    $this->setNewPresent($root, $log);
388  }
Instance->slotSourceProxy
682    return $this->slotSourceProxy ??= \Drupal::service('display_builder.slot_sources_proxy');
Instance->toArray
149      'id' => $this->id,
150      'profileId' => $this->profileId,
151      'contexts' => $this->contexts,
152      'past' => $this->past,
153      'present' => $this->present,
154      'future' => $this->future,
155      'save' => $this->save,
Instance->undo
450    $past = $this->past ?? [];
451
452    if (empty($past)) {
453      return;
456    $present = $this->present;
457    // Remove the last element from the past.
458    $last = \array_pop($past);
459    $this->past = $past;
460    // Set the present to the element we removed in the previous step.
461    $this->present = $last;
462    // Insert the old present state at the beginning of the future.
463    $this->future = \array_merge([$present], $this->future);
464  }