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}

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
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) {
240      foreach ($third_party_settings as $island_id => $settings) {
240      foreach ($third_party_settings as $island_id => $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);
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  }
Instance->attachToSlot
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');
263    if ($third_party_settings) {
264      foreach ($third_party_settings as $island_id => $settings) {
264      foreach ($third_party_settings as $island_id => $settings) {
264      foreach ($third_party_settings as $island_id => $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);
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,
274      '@parent_label' => $parentData ? $this->nodeLabel($parentData) : $parent_id,
274      '@parent_label' => $parentData ? $this->nodeLabel($parentData) : $parent_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  }
Instance->baseFieldDefinitions
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  }
Instance->clear
396    $this->getStorage()->clear($this);
397  }
Instance->currentUser
617    return $this->currentUser ??= \Drupal::service('current_user');
618  }
Instance->getAvailableContexts
363    return $this->getRuntimeContexts([]);
364  }
Instance->getBuildablePlugin
538    $item = $this->get('buildable')->first();
539    /** @var \Drupal\display_builder\DisplayBuildableInterface $buildable */
540    $buildable = $item->getInstance();
541
542    return $buildable;
543  }
Instance->getCurrentState
372    return $this->get('sources')->getValue();
373  }
Instance->getFuture
414    return $this->getStorage()->getFuture($this);
415  }
Instance->getHash
481    if ($hash = $this->get('hash')->first()?->getString()) {
482      return (int) $hash;
485    return NULL;
486  }
Instance->getNode
284  public function getNode(string $node_id): array {
285    return $this->getSourceTree()->getNode($node_id) ?? [];
286  }
Instance->getParentId
291  public function getParentId(string $node_id): ?string {
292    return $this->getSourceTree()->getParentId($node_id);
293  }
Instance->getPast
405    return $this->getStorage()->getPast($this);
406  }
Instance->getPathIndex
474    return $this->getSourceTree()->getPathIndex();
475  }
Instance->getProfile
181    return $this->getBuildablePlugin()->getProfile();
182  }
Instance->getPublishedHash
442    $published_data = $this->getBuildablePlugin()->getSources();
443
444    return $published_data ? self::getUniqId($published_data) : NULL;
444    return $published_data ? self::getUniqId($published_data) : NULL;
444    return $published_data ? self::getUniqId($published_data) : NULL;
444    return $published_data ? self::getUniqId($published_data) : NULL;
445  }
Instance->getPublishedTime
451    $time = (int) $this->get('published')->getString();
452
453    return ($time === 0) ? NULL : $time;
453    return ($time === 0) ? NULL : $time;
453    return ($time === 0) ? NULL : $time;
453    return ($time === 0) ? NULL : $time;
454  }
Instance->getRuntimeContexts
353  public function getRuntimeContexts(array $unqualified_context_ids): ?array {
354    $contexts = $this->getBuildablePlugin()->getRuntimeContexts($unqualified_context_ids) ?? [];
355
356    return $this->refreshContexts($contexts);
357  }
Instance->getSourceTree
583    if ($this->sourceTree === NULL) {
584      $this->sourceTree = new SourceTree($this->getCurrentState());
585    }
586
587    return $this->sourceTree;
587    return $this->sourceTree;
588  }
Instance->getStorage
625    $storage = $this->entityTypeManager()->getStorage('display_builder_instance');
626
627    return $storage;
628  }
Instance->getUniqId
524  public static function getUniqId(array $data): int {
525    $data = self::normalizeRootLevel($data);
526
527    return \crc32((string) \serialize($data));
528  }
Instance->getUsers
421    return $this->getStorage()->getUsers($this);
422  }
Instance->isPublishable
428    $contexts = $this->getAvailableContexts();
429
430    if (!\array_key_exists('context_requirements', $contexts)
431      || !($contexts['context_requirements'] instanceof RequirementsContext)) {
431      || !($contexts['context_requirements'] instanceof RequirementsContext)) {
432      return FALSE;
435    return TRUE;
436  }
Instance->isPublished
460    return (bool) $this->getBuildablePlugin()->getSources();
461  }
Instance->isPublishedPresent
467    return $this->getHash() === $this->getPublishedHash();
468  }
Instance->label
146    $parts = \explode('__', (string) $this->id());
147
148    if (\count($parts) > 1) {
149      \array_shift($parts);
150
151      return \ucfirst(\implode(' ', \str_replace('_', ' ', $parts)));
154    return (string) $this->id();
155  }
Instance->moveToRoot
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;
195    if (!$tree->moveToRoot($node_id, $position)) {
196      return FALSE;
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  }
Instance->moveToSlot
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;
216    if (!$tree->moveToSlot($node_id, $parent_id, $slot_id, $position)) {
217      return FALSE;
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,
224      '@parent_label' => $parentData ? $this->nodeLabel($parentData) : $parent_id,
224      '@parent_label' => $parentData ? $this->nodeLabel($parentData) : $parent_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  }
Instance->nodeLabel
607  private function nodeLabel(array $data): string {
608    $label = $this->slotSourceProxy()->getLabelWithSummary($data, [], TRUE)['label'];
609
610    return $label !== '' ? $label : ($data['source_id'] ?? '');
610    return $label !== '' ? $label : ($data['source_id'] ?? '');
610    return $label !== '' ? $label : ($data['source_id'] ?? '');
610    return $label !== '' ? $label : ($data['source_id'] ?? '');
611  }
Instance->normalizeRootLevel
559  private static function normalizeRootLevel(array $data): array {
560    $data = \array_is_list($data) ? $data : [$data];
560    $data = \array_is_list($data) ? $data : [$data];
560    $data = \array_is_list($data) ? $data : [$data];
560    $data = \array_is_list($data) ? $data : [$data];
561
562    foreach ($data as $index => $source) {
562    foreach ($data as $index => $source) {
562    foreach ($data as $index => $source) {
562    foreach ($data as $index => $source) {
563      \ksort($source);
564      $data[$index] = \array_filter($source);
565    }
566
567    return $data;
568  }
Instance->postCreate
162  public function postCreate(EntityStorageInterface $storage): void {
163    $sources = $this->get('sources')->getValue() ?? [];
164
165    if (empty($sources)) {
166      return;
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  }
Instance->publish
379    $this->getBuildablePlugin()->saveSources();
380    $this->set('published', \Drupal::time()->getRequestTime());
381    $this->setNewRevision(FALSE);
382    $this->save();
383  }
Instance->refreshContexts
643  private function refreshContexts(array $contexts): array {
644    foreach ($contexts as &$context) {
644    foreach ($contexts as &$context) {
645      if ($context instanceof EntityContext) {
649        $entity = $context->getContextValue();
650
651        // Check if sample entity.
652        if ($entity->id()) {
652        if ($entity->id()) {
653          $entity = $this->entityTypeManager()->getStorage($entity->getEntityTypeId())->load($entity->id());
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) {
661        if (!$entity) {
662          return $contexts;
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);
644    foreach ($contexts as &$context) {
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  }
Instance->remove
331  public function remove(string $node_id): void {
332    $tree = $this->getSourceTree();
333    $data = $tree->getNodeData($node_id);
334
335    if (!$data) {
336      return;
338    $parent_id = $tree->getParentId($node_id);
339
340    $tree->remove($node_id);
341
342    $parentData = $parent_id === NULL ? NULL : $tree->getNodeData($parent_id);
342    $parentData = $parent_id === NULL ? NULL : $tree->getNodeData($parent_id);
342    $parentData = $parent_id === NULL ? NULL : $tree->getNodeData($parent_id);
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',
345      '@parent_label' => $parentData ? $this->nodeLabel($parentData) : 'root',
345      '@parent_label' => $parentData ? $this->nodeLabel($parentData) : 'root',
345      '@parent_label' => $parentData ? $this->nodeLabel($parentData) : 'root',
346    ]);
347    $this->setNewPresent($tree->getTree(), $log, FALSE, FALSE);
348  }
Instance->restore
389    $this->setNewPresent($this->getBuildablePlugin()->getSources(), new TranslatableMarkup('Restore published data.'));
390  }
Instance->sampleEntityGenerator
594    return $this->sampleEntityGenerator ??= \Drupal::service('ui_patterns.sample_entity_generator');
595  }
Instance->setNewPresent
493  public function setNewPresent(array $data, string|\Stringable $log_message = '', bool $check_hash = TRUE, bool $index = TRUE): void {
494    if ($index) {
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);
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()) {
505    if ($check_hash && $hash === $this->getHash()) {
505    if ($check_hash && $hash === $this->getHash()) {
506      return;
510    $data = \array_is_list($data) ? $data : [$data];
510    $data = \array_is_list($data) ? $data : [$data];
510    $data = \array_is_list($data) ? $data : [$data];
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  }
Instance->setSource
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');
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  }
Instance->setThirdPartySettings
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;
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  }
Instance->slotSourceProxy
601    return $this->slotSourceProxy ??= \Drupal::service('display_builder.slot_sources_proxy');
602  }