Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
0.00% covered (danger)
0.00%
0 / 59
0.00% covered (danger)
0.00%
0 / 8
CRAP
0.00% covered (danger)
0.00%
0 / 1
ApiPreviewController
0.00% covered (danger)
0.00%
0 / 59
0.00% covered (danger)
0.00%
0 / 8
380
0.00% covered (danger)
0.00%
0 / 1
 __construct
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 getBlockPreview
0.00% covered (danger)
0.00%
0 / 5
0.00% covered (danger)
0.00%
0 / 1
2
 getPresetPreview
0.00% covered (danger)
0.00%
0 / 7
0.00% covered (danger)
0.00%
0 / 1
2
 getComponentPreview
0.00% covered (danger)
0.00%
0 / 15
0.00% covered (danger)
0.00%
0 / 1
12
 generateBlock
0.00% covered (danger)
0.00%
0 / 7
0.00% covered (danger)
0.00%
0 / 1
2
 generateStory
0.00% covered (danger)
0.00%
0 / 9
0.00% covered (danger)
0.00%
0 / 1
6
 generateComponent
0.00% covered (danger)
0.00%
0 / 12
0.00% covered (danger)
0.00%
0 / 1
90
 renderSource
0.00% covered (danger)
0.00%
0 / 3
0.00% covered (danger)
0.00%
0 / 1
2
1<?php
2
3declare(strict_types=1);
4
5namespace Drupal\display_builder\Controller;
6
7use Drupal\Core\Controller\ControllerBase;
8use Drupal\Core\Render\HtmlResponse;
9use Drupal\Core\Render\RendererInterface;
10use Drupal\Core\Theme\ComponentPluginManager;
11use Drupal\display_builder\RenderableBuilderTrait;
12use Drupal\ui_patterns_library\StoryPluginManager;
13use Symfony\Component\DependencyInjection\Attribute\Autowire;
14
15/**
16 * Returns preview responses for Display builder routes.
17 */
18class ApiPreviewController extends ControllerBase {
19
20  use RenderableBuilderTrait;
21
22  /**
23   * The Pattern preset storage.
24   *
25   * @var \Drupal\Core\Entity\EntityStorageInterface
26   */
27  protected $presetConfigStorage;
28
29  public function __construct(
30    #[Autowire(service: 'plugin.manager.component_story')]
31    private StoryPluginManager $storyPluginManager,
32    #[Autowire(service: 'plugin.manager.sdc')]
33    private ComponentPluginManager $componentManager,
34    private RendererInterface $renderer,
35  ) {
36    $this->presetConfigStorage = $this->entityTypeManager()->getStorage('pattern_preset');
37  }
38
39  /**
40   * Get block preview.
41   *
42   * @param string $block_id
43   *   Block ID.
44   *
45   * @return \Drupal\Core\Render\HtmlResponse
46   *   The HTML response.
47   */
48  public function getBlockPreview(string $block_id): HtmlResponse {
49    $build = $this->generateBlock($block_id);
50
51    $html = $this->renderer->renderRoot($build);
52    $response = new HtmlResponse();
53    $response->setContent($html);
54
55    return $response;
56  }
57
58  /**
59   * Get preset preview.
60   *
61   * @param string $preset_id
62   *   Preset ID.
63   *
64   * @return \Drupal\Core\Render\HtmlResponse
65   *   The HTML response.
66   */
67  public function getPresetPreview(string $preset_id): HtmlResponse {
68    /** @var \Drupal\display_builder\PatternPresetInterface $preset */
69    $preset = $this->presetConfigStorage->load($preset_id);
70    $data = $preset->getSources([], FALSE);
71
72    $build = $this->renderSource($data);
73
74    $html = $this->renderer->renderRoot($build);
75    $response = new HtmlResponse();
76    $response->setContent($html);
77
78    return $response;
79  }
80
81  /**
82   * Get component preview, only HTML without specific css or js.
83   *
84   * @param string $component_id
85   *   Component ID.
86   * @param string $variant_id
87   *   Component variant ID.
88   *
89   * @return \Drupal\Core\Render\HtmlResponse
90   *   The HTML response.
91   */
92  public function getComponentPreview(string $component_id, string $variant_id): HtmlResponse {
93    $ui_patterns_library = $this->moduleHandler()->moduleExists('ui_patterns_library');
94
95    $build = [];
96
97    if (!$ui_patterns_library) {
98      $build = $this->generateComponent($component_id);
99    }
100    else {
101      $stories = $this->storyPluginManager->getComponentStories($component_id);
102
103      if (empty($stories)) {
104        $build = $this->generateComponent($component_id);
105      }
106      else {
107        $story = [];
108        $first_story = \reset($stories);
109        $story[$first_story['machineName'] ?? 'default'] = $first_story;
110        $build = $this->generateStory($component_id, $variant_id, $story);
111      }
112    }
113
114    $html = $this->renderer->renderRoot($build);
115    $response = new HtmlResponse();
116    $response->setContent($html);
117
118    return $response;
119  }
120
121  /**
122   * Build renderable block.
123   *
124   * @param string $block_id
125   *   The block id to preview.
126   *
127   * @return array
128   *   A renderable array.
129   */
130  protected function generateBlock(string $block_id): array {
131    $data = [
132      'source_id' => 'block',
133      'source' => [
134        'plugin_id' => $block_id,
135      ],
136    ];
137
138    return $this->renderSource($data);
139  }
140
141  /**
142   * Generate a story.
143   *
144   * @param string $component_id
145   *   The component id.
146   * @param string $variant_id
147   *   The component variant id.
148   * @param array $stories
149   *   The stories.
150   *
151   * @return array
152   *   The render array
153   */
154  private function generateStory(string $component_id, string $variant_id, array $stories): array {
155    $html = [];
156
157    foreach (\array_keys($stories) as $story_id) {
158      $html[$story_id] = [
159        '#type' => 'component',
160        '#component' => $component_id,
161        '#story' => $story_id,
162        '#props' => ['variant' => $variant_id],
163      ];
164    }
165
166    return $html;
167  }
168
169  /**
170   * Generate a content even with empty story or no library module.
171   *
172   * @param string $component_id
173   *   The component id.
174   *
175   * @return array
176   *   The render array
177   *
178   * @todo Remove when https://www.drupal.org/project/ui_patterns/issues/3414774 is merged
179   */
180  private function generateComponent(string $component_id): array {
181    $definition = $this->componentManager->getDefinition($component_id);
182    $html = [
183      '#type' => 'component',
184      '#component' => $component_id,
185    ];
186
187    foreach ($definition['slots'] ?? [] as $slot_id => $slot) {
188      if (isset($slot['examples']) && \is_array($slot['examples']) && !empty($slot['examples'])) {
189        $html['#slots'][$slot_id] = $slot['examples'][0];
190      }
191    }
192
193    foreach ($definition['props']['properties'] ?? [] as $prop_id => $prop) {
194      if (isset($prop['examples']) && \is_array($prop['examples']) && !empty($prop['examples'])) {
195        $html['#props'][$prop_id] = $prop['examples'][0];
196      }
197    }
198
199    return $html;
200  }
201
202  /**
203   * Get renderable array for a slot source.
204   *
205   * @param array $data
206   *   The slot source data array containing:
207   *   - source_id: The source ID
208   *   - source: Array of source configuration.
209   *
210   * @return array
211   *   The renderable array for this slot source.
212   */
213  private function renderSource(array $data): array {
214    /** @var \Drupal\ui_patterns\Element\ComponentElementBuilder $builder */
215    $builder = \Drupal::service('ui_patterns.component_element_builder'); // @phpcs:ignore
216    $build = $builder->buildSource([], 'content', [], $data, []) ?? [];
217
218    return $build['#slots']['content'][0] ?? [];
219  }
220
221}