Code Coverage |
||||||||||||||||
Lines |
Branches |
Paths |
Functions and Methods |
Classes and Traits |
||||||||||||
| Total | |
0.00% |
0 / 260 |
|
0.00% |
0 / 127 |
|
0.00% |
0 / 98 |
|
0.00% |
0 / 44 |
CRAP | |
0.00% |
0 / 1 |
| Instance | |
0.00% |
0 / 260 |
|
0.00% |
0 / 127 |
|
0.00% |
0 / 98 |
|
0.00% |
0 / 44 |
7656 | |
0.00% |
0 / 1 |
| isNew | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
| toArray | |
0.00% |
0 / 9 |
|
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
| postCreate | |
0.00% |
0 / 2 |
|
0.00% |
0 / 3 |
|
0.00% |
0 / 2 |
|
0.00% |
0 / 1 |
6 | |||
| getProfile | |
0.00% |
0 / 2 |
|
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
| setProfile | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
| moveToRoot | |
0.00% |
0 / 14 |
|
0.00% |
0 / 4 |
|
0.00% |
0 / 3 |
|
0.00% |
0 / 1 |
12 | |||
| moveToSlot | |
0.00% |
0 / 23 |
|
0.00% |
0 / 8 |
|
0.00% |
0 / 5 |
|
0.00% |
0 / 1 |
30 | |||
| attachToRoot | |
0.00% |
0 / 16 |
|
0.00% |
0 / 3 |
|
0.00% |
0 / 2 |
|
0.00% |
0 / 1 |
6 | |||
| attachToSlot | |
0.00% |
0 / 19 |
|
0.00% |
0 / 3 |
|
0.00% |
0 / 2 |
|
0.00% |
0 / 1 |
6 | |||
| get | |
0.00% |
0 / 4 |
|
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
| getParentId | |
0.00% |
0 / 4 |
|
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
| setSource | |
0.00% |
0 / 13 |
|
0.00% |
0 / 4 |
|
0.00% |
0 / 3 |
|
0.00% |
0 / 1 |
12 | |||
| setThirdPartySettings | |
0.00% |
0 / 13 |
|
0.00% |
0 / 3 |
|
0.00% |
0 / 2 |
|
0.00% |
0 / 1 |
6 | |||
| remove | |
0.00% |
0 / 13 |
|
0.00% |
0 / 4 |
|
0.00% |
0 / 2 |
|
0.00% |
0 / 1 |
6 | |||
| getContexts | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
| setSave | |
0.00% |
0 / 2 |
|
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
| getCurrentState | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
| restore | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
| undo | |
0.00% |
0 / 8 |
|
0.00% |
0 / 3 |
|
0.00% |
0 / 2 |
|
0.00% |
0 / 1 |
6 | |||
| redo | |
0.00% |
0 / 7 |
|
0.00% |
0 / 3 |
|
0.00% |
0 / 2 |
|
0.00% |
0 / 1 |
6 | |||
| clear | |
0.00% |
0 / 2 |
|
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
| getCountPast | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
| getCountFuture | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
| getUsers | |
0.00% |
0 / 7 |
|
0.00% |
0 / 6 |
|
0.00% |
0 / 5 |
|
0.00% |
0 / 1 |
20 | |||
| canSaveContextsRequirement | |
0.00% |
0 / 7 |
|
0.00% |
0 / 6 |
|
0.00% |
0 / 4 |
|
0.00% |
0 / 1 |
20 | |||
| hasSaveContextsRequirement | |
0.00% |
0 / 7 |
|
0.00% |
0 / 8 |
|
0.00% |
0 / 8 |
|
0.00% |
0 / 1 |
30 | |||
| hasSave | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
| saveIsCurrent | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
| getPathIndex | |
0.00% |
0 / 4 |
|
0.00% |
0 / 3 |
|
0.00% |
0 / 2 |
|
0.00% |
0 / 1 |
6 | |||
| setNewPresent | |
0.00% |
0 / 14 |
|
0.00% |
0 / 6 |
|
0.00% |
0 / 5 |
|
0.00% |
0 / 1 |
20 | |||
| getCurrent | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
| getUniqId | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
| buildIndexFromSlot | |
0.00% |
0 / 4 |
|
0.00% |
0 / 4 |
|
0.00% |
0 / 3 |
|
0.00% |
0 / 1 |
6 | |||
| sampleEntityGenerator | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
| slotSourceProxy | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
| currentUser | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
| refreshContexts | |
0.00% |
0 / 10 |
|
0.00% |
0 / 10 |
|
0.00% |
0 / 7 |
|
0.00% |
0 / 1 |
30 | |||
| changeSourcePositionInSlot | |
0.00% |
0 / 6 |
|
0.00% |
0 / 5 |
|
0.00% |
0 / 4 |
|
0.00% |
0 / 1 |
12 | |||
| getNodeId | |
0.00% |
0 / 5 |
|
0.00% |
0 / 5 |
|
0.00% |
0 / 4 |
|
0.00% |
0 / 1 |
12 | |||
| buildIndexFromSource | |
0.00% |
0 / 16 |
|
0.00% |
0 / 12 |
|
0.00% |
0 / 7 |
|
0.00% |
0 / 1 |
42 | |||
| doAttachToRoot | |
0.00% |
0 / 2 |
|
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
| doAttachToSlot | |
0.00% |
0 / 6 |
|
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
| doRemove | |
0.00% |
0 / 6 |
|
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
| getPath | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
| 1 | <?php |
| 2 | |
| 3 | declare(strict_types=1); |
| 4 | |
| 5 | namespace Drupal\display_builder\Entity; |
| 6 | |
| 7 | use Drupal\Component\Render\FormattableMarkup; |
| 8 | use Drupal\Component\Utility\NestedArray; |
| 9 | use Drupal\Core\Entity\Attribute\EntityType; |
| 10 | use Drupal\Core\Entity\EntityBase; |
| 11 | use Drupal\Core\Entity\EntityStorageInterface; |
| 12 | use Drupal\Core\Entity\EntityTypeManagerInterface; |
| 13 | use Drupal\Core\Plugin\Context\EntityContext; |
| 14 | use Drupal\Core\Session\AccountInterface; |
| 15 | use Drupal\Core\StringTranslation\TranslatableMarkup; |
| 16 | use Drupal\display_builder\HistoryStep; |
| 17 | use Drupal\display_builder\InstanceAccessControlHandler; |
| 18 | use Drupal\display_builder\InstanceInterface; |
| 19 | use Drupal\display_builder\InstanceStorage; |
| 20 | use Drupal\display_builder\ProfileInterface; |
| 21 | use Drupal\display_builder\SlotSourceProxy; |
| 22 | use Drupal\display_builder_ui\InstanceListBuilder; |
| 23 | use Drupal\ui_patterns\Entity\SampleEntityGeneratorInterface; |
| 24 | use 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 | )] |
| 53 | class 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 | } |
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.
| 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']; |
| 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']; |
| 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; |
| 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; |
| 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; |
| 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; |
| 493 | $this->past = []; |
| 494 | $this->future = []; |
| 495 | } |
| 689 | return $this->currentUser ??= \Drupal::service('current_user'); |
| 835 | private function doAttachToRoot(array $root, int $position, array $data): array { |
| 836 | \array_splice($root, $position, 0, [$data]); |
| 837 | |
| 838 | return $root; |
| 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; |
| 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; |
| 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 ?? []; |
| 417 | return $this->refreshContexts($this->contexts); |
| 512 | return \count($this->future); |
| 503 | return \count($this->past); |
| 641 | return $this->present; |
| 434 | return $this->getCurrent()->data ?? []; |
| 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 ''; |
| 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); |
| 901 | private function getPath(array $root, string $node_id): array { |
| 902 | return $this->getPathIndex($root)[$node_id] ?? []; |
| 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 ?? []; |
| 175 | $profile = $this->entityTypeManager()->getStorage('display_builder_profile')->load($this->profileId); |
| 176 | |
| 177 | return $profile; |
| 647 | public static function getUniqId(array $data): int { |
| 648 | return \crc32((string) \serialize($data)); |
| 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; |
| 572 | return !empty($this->save); |
| 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; |
| 139 | return !$this->id(); |
| 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; |
| 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; |
| 164 | public function postCreate(EntityStorageInterface $storage): void { |
| 165 | if ($this->present) { |
| 166 | $this->present->data = $this->buildIndexFromSlot([], $this->getCurrentState()); |
| 167 | } |
| 168 | } |
| 168 | } |
| 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 | } |
| 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; |
| 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 | } |
| 441 | $this->setNewPresent($this->save->data, 'Back to saved data.'); |
| 442 | } |
| 675 | return $this->sampleEntityGenerator ??= \Drupal::service('ui_patterns.sample_entity_generator'); |
| 579 | return $this->present->hash === $this->save->hash; |
| 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 | } |
| 183 | public function setProfile(string $profile_id): void { |
| 184 | $this->profileId = $profile_id; |
| 185 | } |
| 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 | } |
| 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 | } |
| 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 | } |
| 682 | return $this->slotSourceProxy ??= \Drupal::service('display_builder.slot_sources_proxy'); |
| 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, |
| 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 | } |