Code Coverage
 
Lines
Branches
Paths
Functions and Methods
Classes and Traits
Total
84.92% covered (warning)
84.92%
152 / 179
70.16% covered (warning)
70.16%
87 / 124
54.26% covered (warning)
54.26%
51 / 94
67.50% covered (warning)
67.50%
27 / 40
CRAP
0.00% covered (danger)
0.00%
0 / 1
Instance
84.92% covered (warning)
84.92%
152 / 179
70.16% covered (warning)
70.16%
87 / 124
54.26% covered (warning)
54.26%
51 / 94
77.50% covered (warning)
77.50%
31 / 40
613.45
0.00% covered (danger)
0.00%
0 / 1
 baseFieldDefinitions
100.00% covered (success)
100.00%
16 / 16
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
 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
 postCreate
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
 getProfile
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
77.78% covered (warning)
77.78%
7 / 9
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
85.71% covered (warning)
85.71%
12 / 14
62.50% covered (warning)
62.50%
5 / 8
25.00% covered (danger)
25.00%
1 / 4
0.00% covered (danger)
0.00%
0 / 1
10.75
 attachToRoot
77.78% covered (warning)
77.78%
7 / 9
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
87.50% covered (warning)
87.50%
14 / 16
54.55% covered (warning)
54.55%
6 / 11
22.22% covered (danger)
22.22%
2 / 9
0.00% covered (danger)
0.00%
0 / 1
16.76
 getNode
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
 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%
6 / 6
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%
12 / 12
100.00% covered (success)
100.00%
9 / 9
60.00% covered (warning)
60.00%
3 / 5
100.00% covered (success)
100.00%
1 / 1
5.02
 getRuntimeContexts
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
 getAvailableContexts
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
 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
 publish
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
 restore
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
 clear
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
 getPast
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
 getFuture
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
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
 isPublishable
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
 getPublishedHash
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
2
 getPublishedTime
100.00% covered (success)
100.00%
2 / 2
75.00% covered (warning)
75.00%
3 / 4
50.00% covered (danger)
50.00%
1 / 2
100.00% covered (success)
100.00%
1 / 1
2.50
 isPublished
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
 isPublishedPresent
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
 getPathIndex
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
 getHash
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
 setNewPresent
100.00% covered (success)
100.00%
14 / 14
90.00% covered (success)
90.00%
9 / 10
50.00% covered (danger)
50.00%
6 / 12
100.00% covered (success)
100.00%
1 / 1
8.12
 getUniqId
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
 getBuildablePlugin
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
 normalizeRootLevel
100.00% covered (success)
100.00%
5 / 5
85.71% covered (warning)
85.71%
6 / 7
33.33% covered (danger)
33.33%
2 / 6
100.00% covered (success)
100.00%
1 / 1
5.67
 getSourceTree
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
 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
 nodeLabel
100.00% covered (success)
100.00%
2 / 2
75.00% covered (warning)
75.00%
3 / 4
50.00% covered (danger)
50.00%
1 / 2
100.00% covered (success)
100.00%
1 / 1
2.50
 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
 getStorage
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
 refreshContexts
0.00% covered (danger)
0.00%
0 / 10
0.00% covered (danger)
0.00%
0 / 11
0.00% covered (danger)
0.00%
0 / 7
0.00% covered (danger)
0.00%
0 / 1
30
1<?php
2
3declare(strict_types=1);
4
5namespace Drupal\display_builder\Entity;
6
7use Drupal\Core\Entity\Attribute\ContentEntityType;
8use Drupal\Core\Entity\ContentEntityBase;
9use Drupal\Core\Entity\EntityStorageInterface;
10use Drupal\Core\Entity\EntityTypeInterface;
11use Drupal\Core\Entity\EntityTypeManagerInterface;
12use Drupal\Core\Entity\RevisionLogEntityTrait;
13use Drupal\Core\Entity\Sql\SqlContentEntityStorageSchema;
14use Drupal\Core\Field\BaseFieldDefinition;
15use Drupal\Core\Field\FieldStorageDefinitionInterface;
16use Drupal\Core\Plugin\Context\EntityContext;
17use Drupal\Core\Session\AccountInterface;
18use Drupal\Core\StringTranslation\TranslatableMarkup;
19use Drupal\display_builder\DisplayBuildableInterface;
20use Drupal\display_builder\Exception\InvalidNodeException;
21use Drupal\display_builder\InstanceAccessControlHandler;
22use Drupal\display_builder\InstanceInterface;
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;
28
29/**
30 * Defines the display builder instance entity class.
31 */
32#[ContentEntityType(
33  id: 'display_builder_instance',
34  label: new TranslatableMarkup('Display Builder instance'),
35  label_collection: new TranslatableMarkup('Display builder instances'),
36  label_singular: new TranslatableMarkup('display builder instance'),
37  label_plural: new TranslatableMarkup('display builder instances'),
38  entity_keys: [
39    'id' => 'id',
40    'revision' => 'revision',
41    'langcode' => 'langcode',
42    'default_langcode' => 'default_langcode',
43  ],
44  handlers: [
45    'access' => InstanceAccessControlHandler::class,
46    'storage' => InstanceStorage::class,
47    'storage_schema' => SqlContentEntityStorageSchema::class,
48    // Managed by display_builder_ui.
49    'list_builder' => InstanceListBuilder::class,
50  ],
51  links: [
52    // Managed by display_builder_ui.
53    'collection' => '/admin/structure/display-builder/instances',
54  ],
55  label_count: [
56    'singular' => '@count instance',
57    'plural' => '@count instances',
58  ],
59  translatable: TRUE,
60  base_table: 'display_builder_instance',
61  data_table: 'display_builder_instance_field_data',
62  revision_table: 'display_builder_instance_revision',
63  revision_data_table: 'display_builder_instance_field_revision',
64  revision_metadata_keys: [
65    'revision_user' => 'revision_user',
66    'revision_created' => 'revision_created',
67    'revision_log_message' => 'revision_log_message',
68  ],
69)]
70class Instance extends ContentEntityBase implements InstanceInterface {
71
72  use RevisionLogEntityTrait;
73
74  /**
75   * Current user.
76   */
77  public AccountInterface $currentUser;
78
79  /**
80   * Path index.
81   *
82   * A mapping where each key is an slot source node ID and each value has
83   * two properties:
84   * - path: the path
85   * - parent: the node ID of the parent. This is necessary because not every
86   *   SourceWithSlotsInterface implementations has the same "deepness". For
87   *   example, ComponentSource has 4 levels (component, slots, slot_id,
88   *   'sources), LayoutSource has 2 levels (regions, slot_id), etc.
89   */
90  protected array $pathIndex = [];
91
92  /**
93   * Entity type manager.
94   */
95  protected EntityTypeManagerInterface $entityTypeManager;
96
97  /**
98   * Sample entity generator.
99   */
100  protected SampleEntityGeneratorInterface $sampleEntityGenerator;
101
102  /**
103   * Slot source proxy for resolving node labels.
104   */
105  protected SlotSourceProxy $slotSourceProxy;
106
107  /**
108   * Cached normalized source tree for the current present state.
109   *
110   * Stays valid after mutations (index=FALSE path) and is cleared on undo/redo
111   * when the present pointer jumps to a different history step.
112   */
113  protected ?SourceTree $sourceTree = NULL;
114
115  /**
116   * {@inheritdoc}
117   */
118  public static function baseFieldDefinitions(EntityTypeInterface $entity_type) {
119    $fields = parent::baseFieldDefinitions($entity_type);
120    // Add the revision metadata fields.
121    $fields += static::revisionLogBaseFieldDefinitions($entity_type);
122    // Override from ContentEntityBase.
123    $fields['id'] = BaseFieldDefinition::create('string')->setRequired(TRUE)->setReadOnly(TRUE);
124    $fields['buildable'] = BaseFieldDefinition::create('plugin')
125      ->setSetting('plugin_manager_id', 'plugin.manager.display_buildable')
126      ->setRequired(TRUE)
127      ->setReadOnly(TRUE);
128    $fields['sources'] = BaseFieldDefinition::create('ui_patterns_source')
129      ->setRevisionable(TRUE)
130      ->setTranslatable(TRUE)
131      ->setCardinality(FieldStorageDefinitionInterface::CARDINALITY_UNLIMITED);
132    $fields['hash'] = BaseFieldDefinition::create('integer')
133      ->setRevisionable(TRUE)
134      ->setSetting('size', 'big');
135    $fields['published'] = BaseFieldDefinition::create('timestamp');
136
137    return $fields;
138  }
139
140  /**
141   * {@inheritdoc}
142   */
143  public function label() {
144    // Extract a human readable name from an instance id.
145    // Example: "provider__my_display" -> "My display".
146    $parts = \explode('__', (string) $this->id());
147
148    if (\count($parts) > 1) {
149      \array_shift($parts);
150
151      return \ucfirst(\implode(' ', \str_replace('_', ' ', $parts)));
152    }
153
154    return (string) $this->id();
155  }
156
157  /**
158   * {@inheritdoc}
159   *
160   * @see \Drupal\Core\Entity\EntityInterface
161   */
162  public function postCreate(EntityStorageInterface $storage): void {
163    $sources = $this->get('sources')->getValue() ?? [];
164
165    if (empty($sources)) {
166      return;
167    }
168
169    $this->sourceTree = new SourceTree($sources);
170    $indexed = $this->sourceTree->getTree();
171
172    $hash = self::getUniqId($indexed);
173    $this->set('sources', $indexed);
174    $this->set('hash', $hash);
175  }
176
177  /**
178   * {@inheritdoc}
179   */
180  public function getProfile(): ?ProfileInterface {
181    return $this->getBuildablePlugin()->getProfile();
182  }
183
184  /**
185   * {@inheritdoc}
186   */
187  public function moveToRoot(string $node_id, int $position): bool {
188    $tree = $this->getSourceTree();
189    $data = $tree->getNodeData($node_id);
190
191    if (!$data) {
192      return FALSE;
193    }
194
195    if (!$tree->moveToRoot($node_id, $position)) {
196      return FALSE;
197    }
198
199    $log = new TranslatableMarkup('Move @label to root', ['@label' => $this->nodeLabel($data)]);
200    $this->setNewPresent($tree->getTree(), $log, TRUE, FALSE);
201
202    return TRUE;
203  }
204
205  /**
206   * {@inheritdoc}
207   */
208  public function moveToSlot(string $node_id, string $parent_id, string $slot_id, int $position): bool {
209    $tree = $this->getSourceTree();
210    $data = $tree->getNodeData($node_id);
211
212    if (!$data) {
213      return FALSE;
214    }
215
216    if (!$tree->moveToSlot($node_id, $parent_id, $slot_id, $position)) {
217      return FALSE;
218    }
219
220    $parentData = $tree->getNodeData($parent_id);
221    $log = new TranslatableMarkup('Move @label to slot @slot_id in @parent_label', [
222      '@label' => $this->nodeLabel($data),
223      '@slot_id' => $slot_id,
224      '@parent_label' => $parentData ? $this->nodeLabel($parentData) : $parent_id,
225    ]);
226
227    $this->setNewPresent($tree->getTree(), $log, TRUE, FALSE);
228
229    return TRUE;
230  }
231
232  /**
233   * {@inheritdoc}
234   */
235  public function attachToRoot(int $position, string $source_id, array $data, array $third_party_settings = []): string {
236    $tree = $this->getSourceTree();
237    $node_id = $tree->attachToRoot($position, $source_id, $data);
238
239    if ($third_party_settings) {
240      foreach ($third_party_settings as $island_id => $settings) {
241        $tree->setThirdPartySettings($node_id, $island_id, $settings);
242      }
243    }
244
245    $nodeData = $tree->getNodeData($node_id);
246    $log = new TranslatableMarkup('Attach @label to root', ['@label' => $this->nodeLabel($nodeData ?? [])]);
247    $this->setNewPresent($tree->getTree(), $log, FALSE, FALSE);
248
249    return $node_id;
250  }
251
252  /**
253   * {@inheritdoc}
254   */
255  public function attachToSlot(string $parent_id, string $slot_id, int $position, string $source_id, array $data, array $third_party_settings = []): string {
256    $tree = $this->getSourceTree();
257    $node_id = $tree->attachToSlot($parent_id, $slot_id, $position, $source_id, $data);
258
259    if (!$node_id) {
260      throw new InvalidNodeException('Parent or slot not found');
261    }
262
263    if ($third_party_settings) {
264      foreach ($third_party_settings as $island_id => $settings) {
265        $tree->setThirdPartySettings($node_id, $island_id, $settings);
266      }
267    }
268
269    $nodeData = $tree->getNodeData($node_id);
270    $parentData = $tree->getNodeData($parent_id);
271    $log = new TranslatableMarkup('Attach @label to slot @slot_id in @parent_label', [
272      '@label' => $this->nodeLabel($nodeData ?? []),
273      '@slot_id' => $slot_id,
274      '@parent_label' => $parentData ? $this->nodeLabel($parentData) : $parent_id,
275    ]);
276    $this->setNewPresent($tree->getTree(), $log, TRUE, FALSE);
277
278    return $node_id;
279  }
280
281  /**
282   * {@inheritdoc}
283   */
284  public function getNode(string $node_id): array {
285    return $this->getSourceTree()->getNode($node_id) ?? [];
286  }
287
288  /**
289   * {@inheritdoc}
290   */
291  public function getParentId(string $node_id): ?string {
292    return $this->getSourceTree()->getParentId($node_id);
293  }
294
295  /**
296   * {@inheritdoc}
297   */
298  public function setSource(string $node_id, string $source_id, array $data): void {
299    $tree = $this->getSourceTree();
300
301    if (!$tree->setSource($node_id, $source_id, $data)) {
302      throw new InvalidNodeException('Internal node ID mismatch');
303    }
304
305    $nodeData = $tree->getNodeData($node_id);
306    $log = new TranslatableMarkup('Update @label configuration', ['@label' => $this->nodeLabel($nodeData ?? [])]);
307    $this->setNewPresent($tree->getTree(), $log, TRUE, FALSE);
308  }
309
310  /**
311   * {@inheritdoc}
312   */
313  public function setThirdPartySettings(string $node_id, string $island_id, array $data): void {
314    $tree = $this->getSourceTree();
315    $nodeData = $tree->getNodeData($node_id);
316
317    if (!$tree->setThirdPartySettings($node_id, $island_id, $data)) {
318      return;
319    }
320
321    $log = new TranslatableMarkup('Update @island_id configuration in @label', [
322      '@label' => $this->nodeLabel($nodeData),
323      '@island_id' => $island_id,
324    ]);
325    $this->setNewPresent($tree->getTree(), $log, TRUE, FALSE);
326  }
327
328  /**
329   * {@inheritdoc}
330   */
331  public function remove(string $node_id): void {
332    $tree = $this->getSourceTree();
333    $data = $tree->getNodeData($node_id);
334
335    if (!$data) {
336      return;
337    }
338    $parent_id = $tree->getParentId($node_id);
339
340    $tree->remove($node_id);
341
342    $parentData = $parent_id === NULL ? NULL : $tree->getNodeData($parent_id);
343    $log = new TranslatableMarkup('Remove @label from @parent_label', [
344      '@label' => $this->nodeLabel($data),
345      '@parent_label' => $parentData ? $this->nodeLabel($parentData) : 'root',
346    ]);
347    $this->setNewPresent($tree->getTree(), $log, FALSE, FALSE);
348  }
349
350  /**
351   * {@inheritdoc}
352   */
353  public function getRuntimeContexts(array $unqualified_context_ids): ?array {
354    $contexts = $this->getBuildablePlugin()->getRuntimeContexts($unqualified_context_ids) ?? [];
355
356    return $this->refreshContexts($contexts);
357  }
358
359  /**
360   * {@inheritdoc}
361   */
362  public function getAvailableContexts() {
363    return $this->getRuntimeContexts([]);
364  }
365
366  /**
367   * {@inheritdoc}
368   *
369   * @see \Drupal\display_builder\HistoryInterface
370   */
371  public function getCurrentState(): array {
372    return $this->get('sources')->getValue();
373  }
374
375  /**
376   * {@inheritdoc}
377   */
378  public function publish(): void {
379    $this->getBuildablePlugin()->saveSources();
380    $this->set('published', \Drupal::time()->getRequestTime());
381    $this->setNewRevision(FALSE);
382    $this->save();
383  }
384
385  /**
386   * {@inheritdoc}
387   */
388  public function restore(): void {
389    $this->setNewPresent($this->getBuildablePlugin()->getSources(), new TranslatableMarkup('Restore published data.'));
390  }
391
392  /**
393   * {@inheritdoc}
394   */
395  public function clear(): void {
396    $this->getStorage()->clear($this);
397  }
398
399  /**
400   * {@inheritdoc}
401   *
402   * @see \Drupal\display_builder\HistoryInterface
403   */
404  public function getPast(): array {
405    return $this->getStorage()->getPast($this);
406  }
407
408  /**
409   * {@inheritdoc}
410   *
411   * @see \Drupal\display_builder\HistoryInterface
412   */
413  public function getFuture(): array {
414    return $this->getStorage()->getFuture($this);
415  }
416
417  /**
418   * {@inheritdoc}
419   */
420  public function getUsers(): array {
421    return $this->getStorage()->getUsers($this);
422  }
423
424  /**
425   * {@inheritdoc}
426   */
427  public function isPublishable(): bool {
428    $contexts = $this->getAvailableContexts();
429
430    if (!\array_key_exists('context_requirements', $contexts)
431      || !($contexts['context_requirements'] instanceof RequirementsContext)) {
432      return FALSE;
433    }
434
435    return TRUE;
436  }
437
438  /**
439   * {@inheritdoc}
440   */
441  public function getPublishedHash(): ?int {
442    $published_data = $this->getBuildablePlugin()->getSources();
443
444    return $published_data ? self::getUniqId($published_data) : NULL;
445  }
446
447  /**
448   * {@inheritdoc}
449   */
450  public function getPublishedTime(): ?int {
451    $time = (int) $this->get('published')->getString();
452
453    return ($time === 0) ? NULL : $time;
454  }
455
456  /**
457   * {@inheritdoc}
458   */
459  public function isPublished(): bool {
460    return (bool) $this->getBuildablePlugin()->getSources();
461  }
462
463  /**
464   * {@inheritdoc}
465   */
466  public function isPublishedPresent(): bool {
467    return $this->getHash() === $this->getPublishedHash();
468  }
469
470  /**
471   * {@inheritdoc}
472   */
473  public function getPathIndex(): array {
474    return $this->getSourceTree()->getPathIndex();
475  }
476
477  /**
478   * {@inheritdoc}
479   */
480  public function getHash(): ?int {
481    if ($hash = $this->get('hash')->first()?->getString()) {
482      return (int) $hash;
483    }
484
485    return NULL;
486  }
487
488  /**
489   * {@inheritdoc}
490   *
491   * @see \Drupal\display_builder\HistoryInterface
492   */
493  public function setNewPresent(array $data, string|\Stringable $log_message = '', bool $check_hash = TRUE, bool $index = TRUE): void {
494    if ($index) {
495      // Raw data needs normalizing; build tree and keep it as the new cache.
496      $this->sourceTree = new SourceTree($data);
497      $data = $this->sourceTree->getTree();
498    }
499    // When index=FALSE the data was produced by the cached tree's getTree(),
500    // so $this->sourceTree already reflects the new state â€” no invalidation.
501    $hash = self::getUniqId($data);
502
503    // Check if this present is the same to avoid duplicates, for example move
504    // to the same place.
505    if ($check_hash && $hash === $this->getHash()) {
506      return;
507    }
508
509    // Root is always a list of sources.
510    $data = \array_is_list($data) ? $data : [$data];
511
512    $this->setNewRevision(TRUE);
513    $this->set('sources', $data);
514    $this->set('hash', $hash);
515    $this->setRevisionLogMessage((string) $log_message);
516    $this->setRevisionUserId((int) $this->currentUser()->id());
517    $this->setRevisionCreationTime(\time());
518    $this->save();
519  }
520
521  /**
522   * {@inheritdoc}
523   */
524  public static function getUniqId(array $data): int {
525    $data = self::normalizeRootLevel($data);
526
527    return \crc32((string) \serialize($data));
528  }
529
530  /**
531   * Get display buildable plugin.
532   *
533   * @return \Drupal\display_builder\DisplayBuildableInterface
534   *   A display buildable plugin instance.
535   */
536  private function getBuildablePlugin(): DisplayBuildableInterface {
537    /** @var \Drupal\display_builder\Plugin\Field\FieldType\PluginItem $item */
538    $item = $this->get('buildable')->first();
539    /** @var \Drupal\display_builder\DisplayBuildableInterface $buildable */
540    $buildable = $item->getInstance();
541
542    return $buildable;
543  }
544
545  /**
546   * Normalize root level of the data tree.
547   *
548   * ::isPublishedPresent() is comparing hashes from two sources data trees:
549   * - the one from permanent storage where the data is not altered: the hash
550   * of the data stored is the hash of the data retrieved.
551   * - the one from Instance entity where we use a UI Patterns Source field.
552   * Each source is a field item, each property (node_id, source_id, source,
553   * third_party_settings) is a field property. So, Field API can reorder the
554   * properties and fill the missing properties with empty values, altering the
555   * hash calculation and making this comparison difficult.
556   *
557   * To be used only in ::getUniqId().
558   */
559  private static function normalizeRootLevel(array $data): array {
560    $data = \array_is_list($data) ? $data : [$data];
561
562    foreach ($data as $index => $source) {
563      \ksort($source);
564      $data[$index] = \array_filter($source);
565    }
566
567    return $data;
568  }
569
570  /**
571   * Get or create the cached source tree for the current present state.
572   *
573   * The cache is populated lazily on first access and remains valid until
574   * undo() or redo() changes the present pointer to a different history step.
575   * Mutations (index=FALSE path) keep it alive since the tree is the source
576   * of the new present data. The index=TRUE path in setNewPresent() replaces
577   * it with a freshly normalized tree.
578   *
579   * @return \Drupal\display_builder\SourceTree
580   *   The source tree for the current state.
581   */
582  private function getSourceTree(): SourceTree {
583    if ($this->sourceTree === NULL) {
584      $this->sourceTree = new SourceTree($this->getCurrentState());
585    }
586
587    return $this->sourceTree;
588  }
589
590  /**
591   * Sample entity generator.
592   */
593  private function sampleEntityGenerator(): SampleEntityGeneratorInterface {
594    return $this->sampleEntityGenerator ??= \Drupal::service('ui_patterns.sample_entity_generator');
595  }
596
597  /**
598   * Slot source proxy lazy loader.
599   */
600  private function slotSourceProxy(): SlotSourceProxy {
601    return $this->slotSourceProxy ??= \Drupal::service('display_builder.slot_sources_proxy');
602  }
603
604  /**
605   * Returns the human-readable label for a node, falling back to source_id.
606   */
607  private function nodeLabel(array $data): string {
608    $label = $this->slotSourceProxy()->getLabelWithSummary($data, [], TRUE)['label'];
609
610    return $label !== '' ? $label : ($data['source_id'] ?? '');
611  }
612
613  /**
614   * Current user.
615   */
616  private function currentUser(): AccountInterface {
617    return $this->currentUser ??= \Drupal::service('current_user');
618  }
619
620  /**
621   * Get entity storage.
622   */
623  private function getStorage(): InstanceStorage {
624    /** @var \Drupal\display_builder\Entity\InstanceStorage $storage */
625    $storage = $this->entityTypeManager()->getStorage('display_builder_instance');
626
627    return $storage;
628  }
629
630  /**
631   * Refresh contexts after loaded from storage.
632   *
633   * @param \Drupal\Core\Plugin\Context\ContextInterface[] $contexts
634   *   The contexts.
635   *
636   * @throws \Drupal\Component\Plugin\Exception\ContextException
637   * @throws \Drupal\Component\Plugin\Exception\InvalidPluginDefinitionException
638   * @throws \Drupal\Component\Plugin\Exception\PluginNotFoundException
639   *
640   * @return array
641   *   The refreshed contexts or NULL if no context.
642   */
643  private function refreshContexts(array $contexts): array {
644    foreach ($contexts as &$context) {
645      if ($context instanceof EntityContext) {
646        // @todo We should use cache entries here
647        // with the corresponding cache contexts in it.
648        // This may avoid some unnecessary entity loads or generation.
649        $entity = $context->getContextValue();
650
651        // Check if sample entity.
652        if ($entity->id()) {
653          $entity = $this->entityTypeManager()->getStorage($entity->getEntityTypeId())->load($entity->id());
654        }
655        else {
656          $entity = $this->sampleEntityGenerator()->get($entity->getEntityTypeId(), $entity->bundle());
657        }
658
659        // Edge case when the parent entity is deleted but not the builder
660        // instance.
661        if (!$entity) {
662          return $contexts;
663        }
664        $context = (\get_class($context))::fromEntity($entity);
665      }
666    }
667
668    return $contexts;
669  }
670
671}