Code Coverage
 
Lines
Branches
Paths
Functions and Methods
Classes and Traits
Total
94.79% covered (success)
94.79%
91 / 96
81.13% covered (warning)
81.13%
43 / 53
1.95% covered (danger)
1.95%
12 / 616
71.43% covered (warning)
71.43%
5 / 7
CRAP
0.00% covered (danger)
0.00%
0 / 1
ApiContextualMenuController
94.79% covered (success)
94.79%
91 / 96
81.13% covered (warning)
81.13%
43 / 53
1.95% covered (danger)
1.95%
12 / 616
71.43% covered (warning)
71.43%
5 / 7
714.22
0.00% covered (danger)
0.00%
0 / 1
 paste
100.00% covered (success)
100.00%
24 / 24
100.00% covered (success)
100.00%
13 / 13
8.33% covered (danger)
8.33%
2 / 24
100.00% covered (success)
100.00%
1 / 1
24.26
 delete
100.00% covered (success)
100.00%
10 / 10
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
 saveAsPreset
100.00% covered (success)
100.00%
23 / 23
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
3
 pasteStyles
100.00% covered (success)
100.00%
17 / 17
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
2
 deleteStyles
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
 recursiveRefreshNodeId
80.00% covered (warning)
80.00%
4 / 5
87.50% covered (warning)
87.50%
7 / 8
37.50% covered (danger)
37.50%
3 / 8
0.00% covered (danger)
0.00%
0 / 1
7.91
 cleanPreset
66.67% covered (warning)
66.67%
8 / 12
60.87% covered (warning)
60.87%
14 / 23
0.17% covered (danger)
0.17%
1 / 578
0.00% covered (danger)
0.00%
0 / 1
131.37
1<?php
2
3declare(strict_types=1);
4
5namespace Drupal\display_builder\Controller;
6
7use Drupal\display_builder\Event\DisplayBuilderEvents;
8use Drupal\display_builder\InstanceInterface;
9use Symfony\Component\HttpFoundation\Request;
10
11/**
12 * Returns responses for the Display Builder contextual menu routes.
13 */
14class ApiContextualMenuController extends ApiControllerBase implements ApiContextualMenuControllerInterface {
15
16  /**
17   * {@inheritdoc}
18   */
19  public function paste(Request $request, InstanceInterface $display_builder_instance): array {
20    $node_id = (string) $request->request->get('node_id', '');
21    $parent_id = (string) $request->request->get('parent_id', '__root__');
22    $slot_id = (string) $request->request->get('slot_id', '');
23    $slot_position = (string) $request->request->get('slot_position', '0');
24
25    $this->builder = $display_builder_instance;
26    $dataToCopy = $display_builder_instance->getNode($node_id);
27
28    // Keep flag for move or attach to root.
29    $is_paste_root = FALSE;
30    $new_node_id = NULL;
31
32    if (isset($dataToCopy['source_id'], $dataToCopy['source'])) {
33      $source_id = $dataToCopy['source_id'];
34      // Use reference to ensure modifications by recursiveRefreshNodeId
35      // persist.
36      $data = &$dataToCopy['source'];
37
38      // Refresh nested node_ids.
39      self::recursiveRefreshNodeId($data);
40
41      // If no parent we are on root.
42      // @todo for duplicate and not parent root seems not detected and copy is inside the slot.
43      if ($parent_id === '__root__') {
44        $is_paste_root = TRUE;
45        $new_node_id = $display_builder_instance->attachToRoot(0, $source_id, $data, $dataToCopy['third_party_settings'] ?? []);
46      }
47      else {
48        $new_node_id = $display_builder_instance->attachToSlot($parent_id, $slot_id, (int) $slot_position, $source_id, $data, $dataToCopy['third_party_settings'] ?? []);
49      }
50    }
51    $display_builder_instance->save();
52
53    $this->builder = $display_builder_instance;
54
55    return $this->dispatchDisplayBuilderEvent(
56      $is_paste_root ? DisplayBuilderEvents::ON_ATTACH_TO_ROOT : DisplayBuilderEvents::ON_ATTACH_TO_SLOT,
57      NULL,
58      $new_node_id,
59      $is_paste_root ? NULL : $parent_id,
60    );
61  }
62
63  /**
64   * {@inheritdoc}
65   */
66  public function delete(Request $request, InstanceInterface $display_builder_instance): array {
67    $node_id = (string) $request->request->get('node_id', '');
68    $parent_id = $display_builder_instance->getParentId($node_id);
69    $display_builder_instance->remove($node_id);
70    $this->builder = $display_builder_instance;
71
72    return $this->dispatchDisplayBuilderEvent(
73      DisplayBuilderEvents::ON_DELETE,
74      NULL,
75      $node_id,
76      $parent_id
77    );
78  }
79
80  /**
81   * {@inheritdoc}
82   */
83  public function saveAsPreset(Request $request, InstanceInterface $display_builder_instance): array {
84    $node_id = (string) $request->request->get('node_id', '');
85    $label = (string) $this->t('New preset');
86    $data = $display_builder_instance->getNode($node_id);
87    self::cleanPreset($data);
88
89    $preset_storage = $this->entityTypeManager()->getStorage('pattern_preset');
90    $label = $request->headers->get('hx-prompt', $label) ?: $label;
91    // In HTTP headers, only ASCII is guaranteed to work but historically,
92    // HTTP has allowed header values with the ISO-8859-1 charset.
93    $label = \mb_convert_encoding($label, 'UTF-8', 'ISO-8859-1');
94
95    // Build a valid config-entity machine name from the label. Config IDs
96    // must match [a-z0-9_] and must not start with a digit.
97    $base_id = 'preset_' . \preg_replace('/[^a-z0-9_]+/', '_', \mb_strtolower($label));
98    $base_id = \trim($base_id, '_');
99    $id = $base_id;
100    $suffix = 1;
101
102    while ($preset_storage->load($id) !== NULL) {
103      $id = $base_id . '_' . $suffix++;
104    }
105
106    $preset = $preset_storage->create([
107      'id' => $id,
108      'label' => $label,
109      'status' => TRUE,
110      'description' => '',
111      'sources' => $data,
112    ]);
113    $preset->save();
114
115    $this->builder = $display_builder_instance;
116
117    return $this->dispatchDisplayBuilderEvent(DisplayBuilderEvents::ON_PRESET_SAVE);
118  }
119
120  /**
121   * {@inheritdoc}
122   */
123  public function pasteStyles(Request $request, InstanceInterface $display_builder_instance): array {
124    $node_id = (string) $request->request->get('node_id', '');
125    $source_node_id = (string) $request->request->get('source_node_id', '');
126    $mode = (string) $request->request->get('mode', 'replace');
127
128    $default_styles = ['selected' => [], 'extra' => ''];
129    $source = $display_builder_instance->getNode($source_node_id);
130    $styles = $source['third_party_settings']['styles'] ?? $default_styles;
131
132    if ($mode === 'merge') {
133      $target = $display_builder_instance->getNode($node_id);
134      $current = $target['third_party_settings']['styles'] ?? $default_styles;
135      $styles = [
136        // Later array wins on overlapping keys: the copied styles override
137        // the target's own selection for any style category they share,
138        // while categories only the target had are preserved.
139        'selected' => \array_merge($current['selected'] ?? [], $styles['selected'] ?? []),
140        'extra' => \trim(\trim((string) ($current['extra'] ?? '')) . ' ' . \trim((string) ($styles['extra'] ?? ''))),
141      ];
142    }
143
144    $display_builder_instance->setThirdPartySettings($node_id, 'styles', $styles);
145    $display_builder_instance->save();
146
147    $this->builder = $display_builder_instance;
148
149    return $this->dispatchDisplayBuilderEvent(DisplayBuilderEvents::ON_UPDATE, NULL, $node_id);
150  }
151
152  /**
153   * {@inheritdoc}
154   */
155  public function deleteStyles(Request $request, InstanceInterface $display_builder_instance): array {
156    $node_id = (string) $request->request->get('node_id', '');
157    $display_builder_instance->setThirdPartySettings($node_id, 'styles', ['selected' => [], 'extra' => '']);
158    $display_builder_instance->save();
159
160    $this->builder = $display_builder_instance;
161
162    return $this->dispatchDisplayBuilderEvent(DisplayBuilderEvents::ON_UPDATE, NULL, $node_id);
163  }
164
165  /**
166   * Recursively regenerate the node_id key.
167   *
168   * @param array $array
169   *   The array reference.
170   */
171  private static function recursiveRefreshNodeId(array &$array): void {
172    if (isset($array['node_id'])) {
173      $array['node_id'] = \bin2hex(\random_bytes(8));
174    }
175
176    foreach ($array as &$value) {
177      if (\is_array($value)) {
178        self::recursiveRefreshNodeId($value);
179      }
180    }
181  }
182
183  /**
184   * Recursively clean the node data for export or preset saving.
185   *
186   * Unset node_id and remove empty values.
187   *
188   * @param array $array
189   *   The array reference.
190   */
191  private static function cleanPreset(array &$array): void {
192    unset($array['node_id']);
193
194    foreach ($array as $key => &$value) {
195      if (\is_array($value)) {
196        self::cleanPreset($value);
197
198        // Remove empty values to reduce size and noise in the exported preset.
199        if (isset($value['source_id'], $value['source']['value']) && $value['source']['value'] === '') {
200          unset($array[$key]);
201        }
202      }
203
204      if ($key === 'extra' && empty($value)) {
205        unset($array[$key]);
206      }
207
208      if ($key === 'third_party_settings' && empty($value)) {
209        unset($array[$key]);
210      }
211
212      if ($key === 'variant_id' && $value === NULL) {
213        unset($array[$key]);
214      }
215    }
216  }
217
218}

Branches

Below are the source code lines that represent each code branch as identified by Xdebug. Please note a branch is not necessarily coterminous with a line, a line may contain multiple branches and therefore show up more than once. Please also be aware that some branches may be implicit rather than explicit, e.g. an if statement always has an else as part of its logical flow even if you didn't write one.

ApiContextualMenuController->cleanPreset
191  private static function cleanPreset(array &$array): void {
192    unset($array['node_id']);
193
194    foreach ($array as $key => &$value) {
194    foreach ($array as $key => &$value) {
194    foreach ($array as $key => &$value) {
195      if (\is_array($value)) {
196        self::cleanPreset($value);
197
198        // Remove empty values to reduce size and noise in the exported preset.
199        if (isset($value['source_id'], $value['source']['value']) && $value['source']['value'] === '') {
199        if (isset($value['source_id'], $value['source']['value']) && $value['source']['value'] === '') {
199        if (isset($value['source_id'], $value['source']['value']) && $value['source']['value'] === '') {
199        if (isset($value['source_id'], $value['source']['value']) && $value['source']['value'] === '') {
199        if (isset($value['source_id'], $value['source']['value']) && $value['source']['value'] === '') {
200          unset($array[$key]);
201        }
202      }
203
204      if ($key === 'extra' && empty($value)) {
204      if ($key === 'extra' && empty($value)) {
204      if ($key === 'extra' && empty($value)) {
204      if ($key === 'extra' && empty($value)) {
205        unset($array[$key]);
206      }
207
208      if ($key === 'third_party_settings' && empty($value)) {
208      if ($key === 'third_party_settings' && empty($value)) {
208      if ($key === 'third_party_settings' && empty($value)) {
208      if ($key === 'third_party_settings' && empty($value)) {
209        unset($array[$key]);
210      }
211
212      if ($key === 'variant_id' && $value === NULL) {
212      if ($key === 'variant_id' && $value === NULL) {
212      if ($key === 'variant_id' && $value === NULL) {
212      if ($key === 'variant_id' && $value === NULL) {
194    foreach ($array as $key => &$value) {
195      if (\is_array($value)) {
196        self::cleanPreset($value);
197
198        // Remove empty values to reduce size and noise in the exported preset.
199        if (isset($value['source_id'], $value['source']['value']) && $value['source']['value'] === '') {
200          unset($array[$key]);
201        }
202      }
203
204      if ($key === 'extra' && empty($value)) {
205        unset($array[$key]);
206      }
207
208      if ($key === 'third_party_settings' && empty($value)) {
209        unset($array[$key]);
210      }
211
212      if ($key === 'variant_id' && $value === NULL) {
213        unset($array[$key]);
194    foreach ($array as $key => &$value) {
194    foreach ($array as $key => &$value) {
195      if (\is_array($value)) {
196        self::cleanPreset($value);
197
198        // Remove empty values to reduce size and noise in the exported preset.
199        if (isset($value['source_id'], $value['source']['value']) && $value['source']['value'] === '') {
200          unset($array[$key]);
201        }
202      }
203
204      if ($key === 'extra' && empty($value)) {
205        unset($array[$key]);
206      }
207
208      if ($key === 'third_party_settings' && empty($value)) {
209        unset($array[$key]);
210      }
211
212      if ($key === 'variant_id' && $value === NULL) {
213        unset($array[$key]);
214      }
215    }
216  }
ApiContextualMenuController->delete
66  public function delete(Request $request, InstanceInterface $display_builder_instance): array {
67    $node_id = (string) $request->request->get('node_id', '');
68    $parent_id = $display_builder_instance->getParentId($node_id);
69    $display_builder_instance->remove($node_id);
70    $this->builder = $display_builder_instance;
71
72    return $this->dispatchDisplayBuilderEvent(
73      DisplayBuilderEvents::ON_DELETE,
74      NULL,
75      $node_id,
76      $parent_id
77    );
78  }
ApiContextualMenuController->deleteStyles
155  public function deleteStyles(Request $request, InstanceInterface $display_builder_instance): array {
156    $node_id = (string) $request->request->get('node_id', '');
157    $display_builder_instance->setThirdPartySettings($node_id, 'styles', ['selected' => [], 'extra' => '']);
158    $display_builder_instance->save();
159
160    $this->builder = $display_builder_instance;
161
162    return $this->dispatchDisplayBuilderEvent(DisplayBuilderEvents::ON_UPDATE, NULL, $node_id);
163  }
ApiContextualMenuController->paste
19  public function paste(Request $request, InstanceInterface $display_builder_instance): array {
20    $node_id = (string) $request->request->get('node_id', '');
21    $parent_id = (string) $request->request->get('parent_id', '__root__');
22    $slot_id = (string) $request->request->get('slot_id', '');
23    $slot_position = (string) $request->request->get('slot_position', '0');
24
25    $this->builder = $display_builder_instance;
26    $dataToCopy = $display_builder_instance->getNode($node_id);
27
28    // Keep flag for move or attach to root.
29    $is_paste_root = FALSE;
30    $new_node_id = NULL;
31
32    if (isset($dataToCopy['source_id'], $dataToCopy['source'])) {
32    if (isset($dataToCopy['source_id'], $dataToCopy['source'])) {
32    if (isset($dataToCopy['source_id'], $dataToCopy['source'])) {
33      $source_id = $dataToCopy['source_id'];
34      // Use reference to ensure modifications by recursiveRefreshNodeId
35      // persist.
36      $data = &$dataToCopy['source'];
37
38      // Refresh nested node_ids.
39      self::recursiveRefreshNodeId($data);
40
41      // If no parent we are on root.
42      // @todo for duplicate and not parent root seems not detected and copy is inside the slot.
43      if ($parent_id === '__root__') {
43      if ($parent_id === '__root__') {
44        $is_paste_root = TRUE;
48        $new_node_id = $display_builder_instance->attachToSlot($parent_id, $slot_id, (int) $slot_position, $source_id, $data, $dataToCopy['third_party_settings'] ?? []);
49      }
50    }
51    $display_builder_instance->save();
51    $display_builder_instance->save();
52
53    $this->builder = $display_builder_instance;
54
55    return $this->dispatchDisplayBuilderEvent(
56      $is_paste_root ? DisplayBuilderEvents::ON_ATTACH_TO_ROOT : DisplayBuilderEvents::ON_ATTACH_TO_SLOT,
56      $is_paste_root ? DisplayBuilderEvents::ON_ATTACH_TO_ROOT : DisplayBuilderEvents::ON_ATTACH_TO_SLOT,
56      $is_paste_root ? DisplayBuilderEvents::ON_ATTACH_TO_ROOT : DisplayBuilderEvents::ON_ATTACH_TO_SLOT,
56      $is_paste_root ? DisplayBuilderEvents::ON_ATTACH_TO_ROOT : DisplayBuilderEvents::ON_ATTACH_TO_SLOT,
57      NULL,
58      $new_node_id,
59      $is_paste_root ? NULL : $parent_id,
59      $is_paste_root ? NULL : $parent_id,
59      $is_paste_root ? NULL : $parent_id,
59      $is_paste_root ? NULL : $parent_id,
60    );
61  }
ApiContextualMenuController->pasteStyles
123  public function pasteStyles(Request $request, InstanceInterface $display_builder_instance): array {
124    $node_id = (string) $request->request->get('node_id', '');
125    $source_node_id = (string) $request->request->get('source_node_id', '');
126    $mode = (string) $request->request->get('mode', 'replace');
127
128    $default_styles = ['selected' => [], 'extra' => ''];
129    $source = $display_builder_instance->getNode($source_node_id);
130    $styles = $source['third_party_settings']['styles'] ?? $default_styles;
131
132    if ($mode === 'merge') {
133      $target = $display_builder_instance->getNode($node_id);
134      $current = $target['third_party_settings']['styles'] ?? $default_styles;
135      $styles = [
136        // Later array wins on overlapping keys: the copied styles override
137        // the target's own selection for any style category they share,
138        // while categories only the target had are preserved.
139        'selected' => \array_merge($current['selected'] ?? [], $styles['selected'] ?? []),
140        'extra' => \trim(\trim((string) ($current['extra'] ?? '')) . ' ' . \trim((string) ($styles['extra'] ?? ''))),
141      ];
142    }
143
144    $display_builder_instance->setThirdPartySettings($node_id, 'styles', $styles);
144    $display_builder_instance->setThirdPartySettings($node_id, 'styles', $styles);
145    $display_builder_instance->save();
146
147    $this->builder = $display_builder_instance;
148
149    return $this->dispatchDisplayBuilderEvent(DisplayBuilderEvents::ON_UPDATE, NULL, $node_id);
150  }
ApiContextualMenuController->recursiveRefreshNodeId
171  private static function recursiveRefreshNodeId(array &$array): void {
172    if (isset($array['node_id'])) {
173      $array['node_id'] = \bin2hex(\random_bytes(8));
174    }
175
176    foreach ($array as &$value) {
176    foreach ($array as &$value) {
176    foreach ($array as &$value) {
177      if (\is_array($value)) {
176    foreach ($array as &$value) {
177      if (\is_array($value)) {
178        self::recursiveRefreshNodeId($value);
176    foreach ($array as &$value) {
176    foreach ($array as &$value) {
177      if (\is_array($value)) {
178        self::recursiveRefreshNodeId($value);
179      }
180    }
181  }
ApiContextualMenuController->saveAsPreset
83  public function saveAsPreset(Request $request, InstanceInterface $display_builder_instance): array {
84    $node_id = (string) $request->request->get('node_id', '');
85    $label = (string) $this->t('New preset');
86    $data = $display_builder_instance->getNode($node_id);
87    self::cleanPreset($data);
88
89    $preset_storage = $this->entityTypeManager()->getStorage('pattern_preset');
90    $label = $request->headers->get('hx-prompt', $label) ?: $label;
91    // In HTTP headers, only ASCII is guaranteed to work but historically,
92    // HTTP has allowed header values with the ISO-8859-1 charset.
93    $label = \mb_convert_encoding($label, 'UTF-8', 'ISO-8859-1');
94
95    // Build a valid config-entity machine name from the label. Config IDs
96    // must match [a-z0-9_] and must not start with a digit.
97    $base_id = 'preset_' . \preg_replace('/[^a-z0-9_]+/', '_', \mb_strtolower($label));
98    $base_id = \trim($base_id, '_');
99    $id = $base_id;
100    $suffix = 1;
101
102    while ($preset_storage->load($id) !== NULL) {
102    while ($preset_storage->load($id) !== NULL) {
103      $id = $base_id . '_' . $suffix++;
102    while ($preset_storage->load($id) !== NULL) {
106    $preset = $preset_storage->create([
107      'id' => $id,
108      'label' => $label,
109      'status' => TRUE,
110      'description' => '',
111      'sources' => $data,
112    ]);
113    $preset->save();
114
115    $this->builder = $display_builder_instance;
116
117    return $this->dispatchDisplayBuilderEvent(DisplayBuilderEvents::ON_PRESET_SAVE);
118  }