Code Coverage
 
Lines
Branches
Paths
Functions and Methods
Classes and Traits
Total
32.75% covered (danger)
32.75%
94 / 287
39.18% covered (danger)
39.18%
38 / 97
16.22% covered (danger)
16.22%
12 / 74
40.00% covered (danger)
40.00%
6 / 15
CRAP
0.00% covered (danger)
0.00%
0 / 1
ApiController
32.75% covered (danger)
32.75%
94 / 287
39.18% covered (danger)
39.18%
38 / 97
16.22% covered (danger)
16.22%
12 / 74
46.67% covered (danger)
46.67%
7 / 15
1580.75
0.00% covered (danger)
0.00%
0 / 1
 __construct
100.00% covered (success)
100.00%
1 / 1
n/a
0 / 0
n/a
0 / 0
100.00% covered (success)
100.00%
1 / 1
1
 reloadIsland
0.00% covered (danger)
0.00%
0 / 23
0.00% covered (danger)
0.00%
0 / 5
0.00% covered (danger)
0.00%
0 / 3
0.00% covered (danger)
0.00%
0 / 1
12
 attachToRoot
57.58% covered (warning)
57.58%
19 / 33
75.00% covered (warning)
75.00%
12 / 16
22.22% covered (danger)
22.22%
2 / 9
0.00% covered (danger)
0.00%
0 / 1
30.05
 attachToSlot
46.15% covered (danger)
46.15%
18 / 39
56.25% covered (warning)
56.25%
9 / 16
11.11% covered (danger)
11.11%
1 / 9
0.00% covered (danger)
0.00%
0 / 1
41.41
 get
100.00% covered (success)
100.00%
5 / 5
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
 update
21.57% covered (danger)
21.57%
11 / 51
16.67% covered (danger)
16.67%
2 / 12
14.29% covered (danger)
14.29%
1 / 7
0.00% covered (danger)
0.00%
0 / 1
28.67
 thirdPartySettingsUpdate
30.56% covered (danger)
30.56%
11 / 36
66.67% covered (warning)
66.67%
2 / 3
50.00% covered (danger)
50.00%
1 / 2
0.00% covered (danger)
0.00%
0 / 1
2.50
 undo
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
 redo
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
 clear
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
 attachPresetToRoot
0.00% covered (danger)
0.00%
0 / 22
0.00% covered (danger)
0.00%
0 / 8
0.00% covered (danger)
0.00%
0 / 8
0.00% covered (danger)
0.00%
0 / 1
20
 attachPresetToSlot
0.00% covered (danger)
0.00%
0 / 24
0.00% covered (danger)
0.00%
0 / 8
0.00% covered (danger)
0.00%
0 / 8
0.00% covered (danger)
0.00%
0 / 1
20
 resolveSkipIslandId
100.00% covered (success)
100.00%
5 / 5
75.00% covered (warning)
75.00%
6 / 8
40.00% covered (danger)
40.00%
2 / 5
100.00% covered (success)
100.00%
1 / 1
7.46
 validateIslandForm
0.00% covered (danger)
0.00%
0 / 24
0.00% covered (danger)
0.00%
0 / 14
0.00% covered (danger)
0.00%
0 / 17
0.00% covered (danger)
0.00%
0 / 1
56
 responseMessageError
100.00% covered (success)
100.00%
12 / 12
100.00% covered (success)
100.00%
3 / 3
50.00% covered (danger)
50.00%
1 / 2
100.00% covered (success)
100.00%
1 / 1
2.50
1<?php
2
3declare(strict_types=1);
4
5namespace Drupal\display_builder\Controller;
6
7use Drupal\Component\Datetime\TimeInterface;
8use Drupal\Core\Form\FormAjaxException;
9use Drupal\Core\Form\FormState;
10use Drupal\Core\Form\FormStateInterface;
11use Drupal\Core\Render\RendererInterface;
12use Drupal\Core\StringTranslation\TranslatableMarkup;
13use Drupal\Core\TempStore\SharedTempStoreFactory;
14use Drupal\display_builder\Event\DisplayBuilderEvents;
15use Drupal\display_builder\Exception\FormValidationException;
16use Drupal\display_builder\InstanceInterface;
17use Drupal\display_builder\Island\IslandPluginManagerInterface;
18use Drupal\display_builder\Plugin\display_builder\Island\ContextualFormPanel;
19use Drupal\display_builder\RenderableBuilderTrait;
20use Drupal\display_builder\SourceTree;
21use Symfony\Component\DependencyInjection\Attribute\Autowire;
22use Symfony\Component\HttpFoundation\Request;
23use Symfony\Component\HttpFoundation\RequestStack;
24use Symfony\Component\HttpFoundation\Session\SessionInterface;
25use Symfony\Contracts\EventDispatcher\EventDispatcherInterface;
26
27/**
28 * Returns responses for Display builder routes.
29 */
30class ApiController extends ApiControllerBase implements ApiControllerInterface {
31
32  use RenderableBuilderTrait;
33
34  public function __construct(
35    protected EventDispatcherInterface $eventDispatcher,
36    protected RendererInterface $renderer,
37    protected TimeInterface $time,
38    #[Autowire(service: 'tempstore.shared')]
39    protected SharedTempStoreFactory $sharedTempStoreFactory,
40    protected SessionInterface $session,
41    protected RequestStack $requestStack,
42    private IslandPluginManagerInterface $islandPluginManager,
43  ) {
44    parent::__construct($eventDispatcher, $renderer, $time, $sharedTempStoreFactory, $session, $requestStack);
45  }
46
47  /**
48   * {@inheritdoc}
49   */
50  public function reloadIsland(Request $request, InstanceInterface $display_builder_instance, string $island_id): array {
51    $profile = $display_builder_instance->getProfile();
52
53    if (!isset($profile->getEnabledIslands()[$island_id])) {
54      $message = $this->t('[reloadIsland] Island @island is not enabled on this profile', ['@island' => $island_id]);
55      $debug = [
56        'island_id' => $island_id,
57        'instance' => $display_builder_instance,
58      ];
59
60      return $this->responseMessageError((string) $display_builder_instance->id(), $message, $debug);
61    }
62
63    $contexts = $display_builder_instance->getAvailableContexts();
64    $definitions = \array_intersect_key(
65      $this->islandPluginManager->getDefinitions(),
66      [$island_id => TRUE],
67    );
68    $islands = $this->islandPluginManager->createInstances($definitions, $contexts, $profile->getIslandConfigurations());
69    $island = $islands[$island_id] ?? NULL;
70
71    if ($island === NULL) {
72      $message = $this->t('[reloadIsland] Island @island is not available in this context', ['@island' => $island_id]);
73      $debug = [
74        'island_id' => $island_id,
75        'instance' => $display_builder_instance,
76      ];
77
78      return $this->responseMessageError((string) $display_builder_instance->id(), $message, $debug);
79    }
80
81    return [$island->reload($display_builder_instance)];
82  }
83
84  /**
85   * {@inheritdoc}
86   */
87  public function attachToRoot(Request $request, InstanceInterface $display_builder_instance): array {
88    $position = (int) $request->request->get('position', 0);
89
90    if ($request->request->has('preset_id')) {
91      $preset_id = (string) $request->request->get('preset_id');
92
93      return $this->attachPresetToRoot($display_builder_instance, $preset_id, $position);
94    }
95
96    $is_move = FALSE;
97
98    if ($request->request->has('node_id')) {
99      $node_id = (string) $request->request->get('node_id');
100
101      if (!$display_builder_instance->moveToRoot($node_id, $position)) {
102        $message = $this->t('[attachToRoot] moveToRoot failed with invalid data');
103        $debug = [
104          'request' => $request->request->all(),
105          'instance' => $display_builder_instance,
106        ];
107
108        return $this->responseMessageError((string) $display_builder_instance->id(), $message, $debug);
109      }
110
111      $is_move = TRUE;
112    }
113    elseif ($request->request->has('source_id')) {
114      $source_id = (string) $request->request->get('source_id');
115      $data = $request->request->has('source') ? \json_decode((string) $request->request->get('source'), TRUE) : [];
116      $node_id = $display_builder_instance->attachToRoot($position, $source_id, $data);
117    }
118    else {
119      $message = '[attachToRoot] Missing content (source_id, node_id or preset_id)';
120      $debug = [
121        'request' => $request->request->all(),
122        'instance' => $display_builder_instance,
123      ];
124
125      return $this->responseMessageError((string) $display_builder_instance->id(), $message, $debug);
126    }
127    $display_builder_instance->save();
128
129    $this->builder = $display_builder_instance;
130    // Let's refresh when we add new source to get the placeholder replacement.
131    $this->islandId = $this->resolveSkipIslandId($request, $is_move);
132
133    return $this->dispatchDisplayBuilderEvent(
134      $is_move ? DisplayBuilderEvents::ON_MOVE : DisplayBuilderEvents::ON_ATTACH_TO_ROOT,
135      NULL,
136      $node_id,
137    );
138  }
139
140  /**
141   * {@inheritdoc}
142   */
143  public function attachToSlot(Request $request, InstanceInterface $display_builder_instance, string $node_id, string $slot): array {
144    $parent_id = $node_id;
145    $position = (int) $request->request->get('position', 0);
146
147    if ($request->request->has('preset_id')) {
148      $preset_id = (string) $request->request->get('preset_id');
149
150      return $this->attachPresetToSlot($display_builder_instance, $preset_id, $parent_id, $slot, $position);
151    }
152
153    $is_move = FALSE;
154
155    // First, we update the data state.
156    if ($request->request->has('node_id')) {
157      $node_id = (string) $request->request->get('node_id');
158
159      if (!$display_builder_instance->moveToSlot($node_id, $parent_id, $slot, $position)) {
160        $message = $this->t('[attachToSlot] moveToSlot failed with invalid data');
161        $debug = [
162          'node_id' => $node_id,
163          'slot' => $slot,
164          'request' => $request->request->all(),
165          'instance' => $display_builder_instance,
166        ];
167
168        return $this->responseMessageError((string) $display_builder_instance->id(), $message, $debug);
169      }
170
171      $is_move = TRUE;
172    }
173    elseif ($request->request->has('source_id')) {
174      $source_id = (string) $request->request->get('source_id');
175      $data = $request->request->has('source') ? \json_decode((string) $request->request->get('source'), TRUE) : [];
176      $node_id = $display_builder_instance->attachToSlot($parent_id, $slot, $position, $source_id, $data);
177    }
178    else {
179      $message = $this->t('[attachToSlot] Missing content (component_id, block_id or node_id)');
180      $debug = [
181        'node_id' => $node_id,
182        'slot' => $slot,
183        'request' => $request->request->all(),
184        'instance' => $display_builder_instance,
185      ];
186
187      return $this->responseMessageError((string) $display_builder_instance->id(), $message, $debug);
188    }
189    $display_builder_instance->save();
190
191    $this->builder = $display_builder_instance;
192    // Let's refresh when we add new source to get the placeholder replacement.
193    $this->islandId = $this->resolveSkipIslandId($request, $is_move);
194
195    return $this->dispatchDisplayBuilderEvent(
196      $is_move ? DisplayBuilderEvents::ON_MOVE : DisplayBuilderEvents::ON_ATTACH_TO_SLOT,
197      NULL,
198      $node_id,
199      $parent_id,
200    );
201  }
202
203  /**
204   * {@inheritdoc}
205   */
206  public function get(Request $request, InstanceInterface $display_builder_instance, string $node_id): array {
207    $this->builder = $display_builder_instance;
208
209    return $this->dispatchDisplayBuilderEvent(
210      DisplayBuilderEvents::ON_ACTIVE,
211      $display_builder_instance->getNode($node_id),
212    );
213  }
214
215  /**
216   * {@inheritdoc}
217   */
218  public function update(Request $request, InstanceInterface $display_builder_instance, string $node_id): array {
219    $this->builder = $display_builder_instance;
220    $body = $request->getPayload()->all();
221
222    if (!isset($body['form_id'])) {
223      $message = $this->t('[update] Missing payload!');
224      $debug = [
225        'node_id' => $node_id,
226        'request' => $request->request->all(),
227        'body' => $body,
228        'instance' => $display_builder_instance,
229      ];
230
231      return $this->responseMessageError((string) $display_builder_instance->id(), $message, $debug);
232    }
233
234    // Load the node to properly alter the form data into config data.
235    $node = $display_builder_instance->getNode($node_id);
236
237    if (isset($body['source']['form_build_id'])) {
238      unset($body['source']['form_build_id'], $body['source']['form_token'], $body['source']['form_id']);
239    }
240
241    if (isset($body['form_build_id'])) {
242      unset($body['form_build_id'], $body['form_token'], $body['form_id']);
243    }
244    $form_state = new FormState();
245    // Default values are the existing values from the state.
246    $form_state->addBuildInfo('args', [
247      [
248        'island_id' => 'contextual_form',
249        'builder_id' => (string) $display_builder_instance->id(),
250        'instance' => $node,
251      ],
252      $display_builder_instance->getAvailableContexts(),
253    ]);
254    $form_state->setTemporaryValue('gathered_contexts', $display_builder_instance->getAvailableContexts());
255    // The body received corresponds to raw form values.
256    // We need to set them in the form state to properly
257    // take them into account.
258    $form_state->setValues($body);
259
260    $formClass = ContextualFormPanel::getFormClass();
261    $data = [];
262
263    try {
264      $values = $this->validateIslandForm($formClass, $form_state);
265      $data['source'] = $values;
266    }
267    catch (FormAjaxException $e) {
268      throw $e;
269    }
270    catch (\Exception $e) {
271      $debug = [
272        'node_id' => $node_id,
273        'request' => $request->request->all(),
274        'form' => $form_state->getValues(),
275        'body' => $body,
276        'instance' => $display_builder_instance,
277      ];
278
279      return $this->responseMessageError((string) $display_builder_instance->id(), $e->getMessage(), $debug);
280    }
281
282    $display_builder_instance->setSource($node_id, $node['source_id'], $data['source']);
283    $display_builder_instance->save();
284
285    $this->builder = $display_builder_instance;
286    $this->islandId = (string) $request->query->get('from', NULL);
287
288    return $this->dispatchDisplayBuilderEvent(
289      DisplayBuilderEvents::ON_UPDATE,
290      NULL,
291      $node_id,
292    );
293  }
294
295  /**
296   * {@inheritdoc}
297   */
298  public function thirdPartySettingsUpdate(Request $request, InstanceInterface $display_builder_instance, string $node_id, string $island_id): array {
299    $body = $request->getPayload()->all();
300
301    if (!isset($body['form_id'])) {
302      $message = $this->t('[thirdPartySettingsUpdate] Missing payload!');
303      $debug = [
304        'node_id' => $node_id,
305        'island_id' => $island_id,
306        'request' => $request->request->all(),
307        'body' => $body,
308        'instance' => $display_builder_instance,
309      ];
310
311      return $this->responseMessageError((string) $display_builder_instance->id(), $message, $debug);
312    }
313
314    $islandDefinition = $this->islandPluginManager->getDefinition($island_id);
315    // Load the instance to properly alter the form data into config data.
316    $node = $display_builder_instance->getNode($node_id);
317    unset($body['form_build_id'], $body['form_token'], $body['form_id']);
318
319    $form_state = new FormState();
320    // Default values are the existing values from the state.
321    $form_state->addBuildInfo('args', [
322      [
323        'island_id' => $island_id,
324        'builder_id' => (string) $display_builder_instance->id(),
325        'instance' => $node,
326      ],
327      [],
328    ]);
329
330    // phpcs:disable Drupal.Files.LineLength.TooLong
331    // @todo should context be injected for third party settings?
332    // $form_state->setTemporaryValue('gathered_contexts', $display_builder_instance->getAvailableContexts());
333    // phpcs:enable Drupal.Files.LineLength.TooLong
334    // The body received corresponds to raw form values.
335    // We need to set them in the form state to properly
336    // take them into account.
337    $form_state->setValues($body);
338
339    $formClass = ($islandDefinition['class'])::getFormClass();
340    $values = $this->validateIslandForm($formClass, $form_state);
341    // We update the state with the new data.
342    $display_builder_instance->setThirdPartySettings($node_id, $island_id, $values);
343    $display_builder_instance->save();
344
345    $this->builder = $display_builder_instance;
346    $this->islandId = $island_id;
347
348    return $this->dispatchDisplayBuilderEvent(
349      DisplayBuilderEvents::ON_UPDATE,
350      NULL,
351      $node_id,
352      NULL,
353    );
354  }
355
356  /**
357   * {@inheritdoc}
358   */
359  public function undo(Request $request, InstanceInterface $display_builder_instance): array {
360    /** @var \Drupal\display_builder\Entity\InstanceStorage $storage */
361    $storage = $this->entityTypeManager()->getStorage('display_builder_instance');
362    /** @var \Drupal\display_builder\InstanceInterface $display_builder_instance */
363    $display_builder_instance = $storage->undo($display_builder_instance);
364
365    $this->builder = $display_builder_instance;
366
367    return $this->dispatchDisplayBuilderEvent(DisplayBuilderEvents::ON_HISTORY_CHANGE);
368  }
369
370  /**
371   * {@inheritdoc}
372   */
373  public function redo(Request $request, InstanceInterface $display_builder_instance): array {
374    /** @var \Drupal\display_builder\Entity\InstanceStorage $storage */
375    $storage = $this->entityTypeManager()->getStorage('display_builder_instance');
376    /** @var \Drupal\display_builder\InstanceInterface $display_builder_instance */
377    $display_builder_instance = $storage->redo($display_builder_instance);
378
379    $this->builder = $display_builder_instance;
380
381    return $this->dispatchDisplayBuilderEvent(DisplayBuilderEvents::ON_HISTORY_CHANGE);
382  }
383
384  /**
385   * {@inheritdoc}
386   */
387  public function clear(Request $request, InstanceInterface $display_builder_instance): array {
388    $display_builder_instance->clear();
389    $display_builder_instance->save();
390
391    $this->builder = $display_builder_instance;
392
393    return $this->dispatchDisplayBuilderEvent(DisplayBuilderEvents::ON_HISTORY_CHANGE);
394  }
395
396  /**
397   * Attach a pattern preset to root.
398   *
399   * Presets are "resolved" after attachment, so they are never moved around.
400   *
401   * @param \Drupal\display_builder\InstanceInterface $display_builder_instance
402   *   Display builder instance.
403   * @param string $preset_id
404   *   Pattern preset ID.
405   * @param int $position
406   *   Position.
407   *
408   * @return array
409   *   A renderable array.
410   */
411  protected function attachPresetToRoot(InstanceInterface $display_builder_instance, string $preset_id, int $position): array {
412    $presetStorage = $this->entityTypeManager()->getStorage('pattern_preset');
413
414    /** @var \Drupal\display_builder\Entity\PatternPresetInterface $preset */
415    $preset = $presetStorage->load($preset_id);
416    $data = $preset->getSources();
417
418    if (!isset($data['source_id']) || !isset($data['source'])) {
419      $message = $this->t('[attachToRoot] Missing preset source_id data');
420      $debug = [
421        'preset_id' => $preset_id,
422        'position' => $position,
423        'data' => $data,
424        'instance' => $display_builder_instance,
425      ];
426
427      return $this->responseMessageError((string) $display_builder_instance->id(), $message, $debug);
428    }
429    $node_id = $display_builder_instance->attachToRoot($position, $data['source_id'], $data['source']);
430
431    foreach ($data['third_party_settings'] ?? [] as $provider => $settings) {
432      $display_builder_instance->setThirdPartySettings($node_id, $provider, $settings ?? []);
433    }
434    $display_builder_instance->save();
435    $this->builder = $display_builder_instance;
436
437    return $this->dispatchDisplayBuilderEvent(
438      DisplayBuilderEvents::ON_ATTACH_TO_ROOT,
439      NULL,
440      $node_id,
441    );
442  }
443
444  /**
445   * Attach a pattern preset to a slot .
446   *
447   * Presets are "resolved" after attachment, so they are never moved around.
448   *
449   * @param \Drupal\display_builder\InstanceInterface $display_builder_instance
450   *   Display builder instance.
451   * @param string $preset_id
452   *   Pattern preset ID.
453   * @param string $parent_id
454   *   Parent instance ID.
455   * @param string $slot
456   *   Slot.
457   * @param int $position
458   *   Position.
459   *
460   * @return array
461   *   A renderable array.
462   */
463  protected function attachPresetToSlot(InstanceInterface $display_builder_instance, string $preset_id, string $parent_id, string $slot, int $position): array {
464    $presetStorage = $this->entityTypeManager()->getStorage('pattern_preset');
465
466    /** @var \Drupal\display_builder\Entity\PatternPresetInterface $preset */
467    $preset = $presetStorage->load($preset_id);
468    $data = $preset->getSources();
469
470    if (!isset($data['source_id']) || !isset($data['source'])) {
471      $message = $this->t('[attachToSlot] Missing preset source_id data');
472      $debug = [
473        'preset_id' => $preset_id,
474        'parent_id' => $parent_id,
475        'slot' => $slot,
476        'position' => $position,
477        'data' => $data,
478        'instance' => $display_builder_instance,
479      ];
480
481      return $this->responseMessageError((string) $display_builder_instance->id(), $message, $debug);
482    }
483    $node_id = $display_builder_instance->attachToSlot($parent_id, $slot, $position, $data['source_id'], $data['source']);
484
485    foreach ($data['third_party_settings'] ?? [] as $provider => $settings) {
486      $display_builder_instance->setThirdPartySettings($node_id, $provider, $settings ?? []);
487    }
488
489    $display_builder_instance->save();
490    $this->builder = $display_builder_instance;
491
492    return $this->dispatchDisplayBuilderEvent(
493      DisplayBuilderEvents::ON_ATTACH_TO_ROOT,
494      NULL,
495      $node_id,
496    );
497  }
498
499  /**
500   * Determines which island (if any) to skip from the event dispatch fan-out.
501   *
502   * Builder, Wireframe, and Tree share the same Sortable group (@see
503   * components/dropzone/dropzone.js), so an existing node can be dragged
504   * from one panel's dropzone straight into another's. Sortable only
505   * relocates the DOM node - it never re-renders it - so after a
506   * cross-panel move the dropped element is still wearing whichever
507   * panel originally rendered it (@see
508   * BuilderPanel::buildNodeAttributes(), display_builder.js's addVals()).
509   * Skipping the destination island's own re-render is only safe when the
510   * move stayed within that same island; otherwise its dropzone would be
511   * left showing another island's markup until the next full reload.
512   *
513   * @param \Symfony\Component\HttpFoundation\Request $request
514   *   The request.
515   * @param bool $is_move
516   *   Whether this attach is actually moving an existing node.
517   *
518   * @return string|null
519   *   The island ID to skip, or NULL to skip none.
520   */
521  private function resolveSkipIslandId(Request $request, bool $is_move): ?string {
522    if (!$is_move) {
523      return NULL;
524    }
525
526    $destination_island = (string) $request->query->get('from', '');
527    $source_island = (string) $request->request->get('source_island', '');
528
529    return ($source_island !== '' && $source_island === $destination_island) ? $destination_island : NULL;
530  }
531
532  /**
533   * Validates an island form.
534   *
535   * @param string $formClass
536   *   The form class.
537   * @param \Drupal\Core\Form\FormStateInterface $form_state
538   *   The form state.
539   *
540   * @return array
541   *   The validated values.
542   */
543  private function validateIslandForm(string $formClass, FormStateInterface $form_state): array {
544    /** @var \Drupal\Core\Form\FormBuilder $formBuilder */
545    $formBuilder = $this->formBuilder();
546
547    try {
548      $triggering_element = $form_state->getTriggeringElement();
549
550      if (!$triggering_element && !isset($form_state->getValues()['_triggering_element_name'])) {
551        // We set a fake triggering element to avoid form API error.
552        $form_state->setTriggeringElement([
553          '#type' => 'submit',
554          '#limit_validation_errors' => FALSE,
555          '#value' => (string) $this->t('Submit'),
556        ]);
557      }
558      $form = $formBuilder->buildForm($formClass, $form_state);
559      $formErrors = $form_state->getErrors();
560
561      if (!empty($formErrors)) {
562        $first_error = \reset($formErrors);
563
564        throw new FormValidationException((string) $first_error);
565      }
566      $formBuilder->validateForm($formClass, $form, $form_state);
567      $formErrors = $form_state->getErrors();
568
569      if (!empty($formErrors)) {
570        $first_error = \reset($formErrors);
571
572        throw new FormValidationException((string) $first_error);
573      }
574    }
575    catch (FormAjaxException $e) {
576      throw $e;
577    }
578    // Those values are the validated values, produced by the form.
579    // with all Form API processing.
580    $values = $form_state->getValues();
581
582    // We clean the values from form API keys.
583    if (isset($values['form_build_id'])) {
584      unset($values['form_build_id'], $values['form_token'], $values['form_id']);
585    }
586
587    return $values;
588  }
589
590  /**
591   * Render an error message in the display builder.
592   *
593   * @param string $display_builder_instance_id
594   *   The builder ID.
595   * @param string|\Drupal\Core\StringTranslation\TranslatableMarkup $message
596   *   The error message.
597   * @param array $debug
598   *   The debug code related to the error.
599   *
600   * @return array
601   *   A renderable array.
602   */
603  private function responseMessageError(
604    string $display_builder_instance_id,
605    string|TranslatableMarkup $message,
606    array $debug,
607  ): array {
608    // Reduce verbosity.
609    unset($debug['request']['ajax_page_state']['libraries']);
610
611    $instance = $debug['instance'] ?? NULL;
612
613    if ($instance) {
614      $tree = new SourceTree($debug['instance']->getCurrentState());
615      unset($debug['instance']);
616      $debug['tree'] = $tree->getNormalizedStructure()['structure'];
617    }
618
619    $this->getLogger('display_builder')->error('@message <pre>@debug</pre>', [
620      '@message' => $message,
621      '@debug' => \print_r($debug, TRUE),
622    ]);
623
624    $message = new TranslatableMarkup('An error occurred, refresh the current page to continue from a valid state.<br>Check logs for more details.');
625
626    return $this->buildError($display_builder_instance_id, $message, TRUE);
627  }
628
629}