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