Code Coverage
 
Lines
Branches
Paths
Functions and Methods
Classes and Traits
Total
0.00% covered (danger)
0.00%
0 / 112
0.00% covered (danger)
0.00%
0 / 53
0.00% covered (danger)
0.00%
0 / 43
0.00% covered (danger)
0.00%
0 / 9
CRAP
0.00% covered (danger)
0.00%
0 / 1
BlockLibraryPanel
0.00% covered (danger)
0.00%
0 / 106
0.00% covered (danger)
0.00%
0 / 53
0.00% covered (danger)
0.00%
0 / 43
0.00% covered (danger)
0.00%
0 / 9
812
0.00% covered (danger)
0.00%
0 / 1
 create
0.00% covered (danger)
0.00%
0 / 3
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 defaultConfiguration
0.00% covered (danger)
0.00%
0 / 7
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 buildConfigurationForm
0.00% covered (danger)
0.00%
0 / 8
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 configurationSummary
0.00% covered (danger)
0.00%
0 / 6
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
6
 build
0.00% covered (danger)
0.00%
0 / 20
0.00% covered (danger)
0.00%
0 / 4
0.00% covered (danger)
0.00%
0 / 3
0.00% covered (danger)
0.00%
0 / 1
6
 buildCategorySection
0.00% covered (danger)
0.00%
0 / 13
0.00% covered (danger)
0.00%
0 / 9
0.00% covered (danger)
0.00%
0 / 8
0.00% covered (danger)
0.00%
0 / 1
20
 getSources
0.00% covered (danger)
0.00%
0 / 23
0.00% covered (danger)
0.00%
0 / 15
0.00% covered (danger)
0.00%
0 / 12
0.00% covered (danger)
0.00%
0 / 1
56
 getProvidersOptions
0.00% covered (danger)
0.00%
0 / 8
0.00% covered (danger)
0.00%
0 / 4
0.00% covered (danger)
0.00%
0 / 3
0.00% covered (danger)
0.00%
0 / 1
6
 getProviders
0.00% covered (danger)
0.00%
0 / 18
0.00% covered (danger)
0.00%
0 / 17
0.00% covered (danger)
0.00%
0 / 13
0.00% covered (danger)
0.00%
0 / 1
72
1<?php
2
3declare(strict_types=1);
4
5namespace Drupal\display_builder\Plugin\display_builder\Island;
6
7use Drupal\Core\Extension\ModuleExtensionList;
8use Drupal\Core\Form\FormStateInterface;
9use Drupal\Core\StringTranslation\TranslatableMarkup;
10use Drupal\display_builder\Attribute\Island;
11use Drupal\display_builder\BlockLibrarySourceHelper;
12use Drupal\display_builder\InstanceInterface;
13use Drupal\display_builder\Island\IslandConfigurationFormInterface;
14use Drupal\display_builder\Island\IslandConfigurationFormTrait;
15use Drupal\display_builder\Island\IslandPluginBase;
16use Drupal\display_builder\Island\IslandType;
17use Drupal\display_builder\SourceWithSlotsInterface;
18use Drupal\ui_patterns\SourcePluginBase;
19use Drupal\ui_patterns\SourceWithChoicesInterface;
20use Symfony\Component\DependencyInjection\ContainerInterface;
21
22/**
23 * Block library island plugin implementation.
24 */
25#[Island(
26  id: 'block_library',
27  enabled_by_default: TRUE,
28  label: new TranslatableMarkup('Blocks'),
29  description: new TranslatableMarkup('List of available blocks.'),
30  type: IslandType::Library,
31)]
32class BlockLibraryPanel extends IslandPluginBase implements IslandConfigurationFormInterface {
33
34  use IslandConfigurationFormTrait;
35
36  private const HIDE_SOURCE = [
37    'component',
38    // @todo enable Wysiwyg from our UI when #3561474 is fixed.
39    'wysiwyg',
40  ];
41
42  private const HIDE_PROVIDER = ['ui_patterns_blocks'];
43
44  /**
45   * The sources.
46   */
47  protected array $sources = [];
48
49  /**
50   * The module extension list service.
51   */
52  protected ModuleExtensionList $moduleList;
53
54  /**
55   * {@inheritdoc}
56   */
57  public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition): static {
58    $instance = parent::create($container, $configuration, $plugin_id, $plugin_definition);
59    $instance->moduleList = $container->get('extension.list.module');
60
61    return $instance;
62  }
63
64  /**
65   * {@inheritdoc}
66   */
67  public function defaultConfiguration(): array {
68    return [
69      'exclude' => [
70        'devel',
71        'htmx',
72        'shortcut',
73      ],
74    ];
75  }
76
77  /**
78   * {@inheritdoc}
79   */
80  public function buildConfigurationForm(array $form, FormStateInterface $form_state): array {
81    $configuration = $this->getConfiguration();
82
83    $form['exclude'] = [
84      '#type' => 'checkboxes',
85      '#title' => $this->t('Exclude modules'),
86      '#options' => $this->getProvidersOptions(),
87      '#default_value' => $configuration['exclude'],
88    ];
89
90    return $form;
91  }
92
93  /**
94   * {@inheritdoc}
95   */
96  public function configurationSummary(): array {
97    $configuration = $this->getConfiguration();
98
99    return [
100      $this->t('Excluded modules: @exclude', [
101        '@exclude' => \implode(', ', \array_filter($configuration['exclude'] ?? []) ?: [$this->t('None')]),
102      ]),
103    ];
104  }
105
106  /**
107   * {@inheritdoc}
108   */
109  public function build(InstanceInterface $builder, array $data = [], array $options = []): array {
110    $builder_id = (string) $builder->id();
111    $configuration = $this->getConfiguration();
112
113    $exclude_providers = \array_merge(
114      $configuration['exclude'] ?? [],
115      self::HIDE_PROVIDER
116    );
117    $categories = BlockLibrarySourceHelper::getGroupedChoices(
118      $this->getSources(),
119      $exclude_providers,
120    );
121
122    $build = [];
123
124    foreach ($categories as $category_data) {
125      $build[] = $this->buildCategorySection($category_data, $builder_id);
126    }
127
128    return [
129      '#type' => 'component',
130      '#component' => 'display_builder:library_panel',
131      '#slots' => [
132        'content' => $this->buildDraggables($builder_id, $build),
133      ],
134    ];
135  }
136
137  /**
138   * Build a category section.
139   *
140   * @param array $category_data
141   *   The category data.
142   * @param string $builder_id
143   *   The builder ID.
144   *
145   * @return array
146   *   The render array for the category section.
147   */
148  private function buildCategorySection(array $category_data, string $builder_id): array {
149    $section = [];
150
151    if (!empty($category_data['label'])) {
152      $section[] = [
153        '#type' => 'html_tag',
154        '#tag' => 'h4',
155        '#attributes' => ['class' => 'db-filter-hide-on-search'],
156        '#value' => $category_data['label'],
157      ];
158    }
159
160    foreach ($category_data['choices'] as $choice) {
161      $section[] = $choice['preview']
162        ? $this->buildPlaceholderButtonWithPreview($builder_id, $choice['label'], $choice['data'] ?? [], $choice['preview'], $choice['keywords'] ?? '')
163        : $this->buildPlaceholderButton($choice['label'], $choice['data'] ?? [], $choice['keywords'] ?? '');
164    }
165
166    return $section;
167  }
168
169  /**
170   * Returns all possible sources.
171   *
172   * @throws \Drupal\Component\Plugin\Exception\PluginException
173   *
174   * @return array<string, array>
175   *   An array of sources.
176   */
177  private function getSources(): array {
178    if (!empty($this->sources)) {
179      return $this->sources;
180    }
181
182    $definitions = $this->sourceManager->getDefinitionsForPropType('slot', $this->configuration['contexts'] ?? []);
183    $slot_definition = ['ui_patterns' => ['type_definition' => $this->sourceManager->getSlotPropType()]];
184
185    foreach ($definitions as $source_id => $definition) {
186      // A block is a source for slots but without slots.
187      if (\is_a($definition['class'], SourceWithSlotsInterface::class, TRUE)) {
188        continue;
189      }
190
191      if (\in_array($source_id, self::HIDE_SOURCE, TRUE)) {
192        continue;
193      }
194
195      try {
196        $source = $this->sourceManager->createInstance(
197          $source_id,
198          SourcePluginBase::buildConfiguration('slot', $slot_definition, ['source' => []], $this->configuration['contexts'] ?? [])
199        );
200      }
201      catch (\Throwable $e) {
202        $this->logger->error('Invalid source found: %message', ['%message' => $e->getMessage()]);
203
204        continue;
205      }
206
207      $this->sources[$source_id] = [
208        'definition' => $definition,
209        'source' => $source,
210      ];
211
212      if ($source instanceof SourceWithChoicesInterface) {
213        $this->sources[$source_id]['choices'] = $source->getChoices();
214      }
215    }
216
217    return $this->sources;
218  }
219
220  /**
221   * Get providers options for select input.
222   *
223   * @return array
224   *   An associative array with module ID as key and module description as
225   *   value.
226   */
227  private function getProvidersOptions(): array {
228    $options = [];
229
230    foreach ($this->getProviders() as $provider_id => $provider) {
231      $params = [
232        '@name' => $provider['name'],
233        '@count' => $provider['count'],
234      ];
235      $options[$provider_id] = $this->formatPlural($provider['count'], '@name (@count block)', '@name (@count blocks)', $params);
236    }
237
238    return $options;
239  }
240
241  /**
242   * Get all providers.
243   *
244   * @return array
245   *   Drupal modules definitions, keyed by extension ID
246   */
247  private function getProviders(): array {
248    $sources = $this->getSources();
249    $providers = [];
250    $modules = $this->moduleList->getAllInstalledInfo();
251
252    foreach ($sources as $source_data) {
253      if (!isset($source_data['choices'])) {
254        continue;
255      }
256      $choices = $source_data['choices'];
257
258      foreach ($choices as $choice) {
259        $provider = $choice['provider'] ?? '';
260
261        if (!$provider || \in_array($provider, self::HIDE_PROVIDER, TRUE)) {
262          continue;
263        }
264
265        if (!isset($modules[$provider])) {
266          // If the provider is not a module, skip it.
267          continue;
268        }
269
270        if (!isset($providers[$provider])) {
271          $providers[$provider] = $modules[$provider];
272          $providers[$provider]['count'] = 0;
273        }
274        ++$providers[$provider]['count'];
275      }
276    }
277
278    return $providers;
279  }
280
281}

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.

BlockLibraryPanel->build
109  public function build(InstanceInterface $builder, array $data = [], array $options = []): array {
110    $builder_id = (string) $builder->id();
111    $configuration = $this->getConfiguration();
112
113    $exclude_providers = \array_merge(
114      $configuration['exclude'] ?? [],
115      self::HIDE_PROVIDER
116    );
117    $categories = BlockLibrarySourceHelper::getGroupedChoices(
118      $this->getSources(),
119      $exclude_providers,
120    );
121
122    $build = [];
123
124    foreach ($categories as $category_data) {
124    foreach ($categories as $category_data) {
124    foreach ($categories as $category_data) {
125      $build[] = $this->buildCategorySection($category_data, $builder_id);
124    foreach ($categories as $category_data) {
125      $build[] = $this->buildCategorySection($category_data, $builder_id);
126    }
127
128    return [
129      '#type' => 'component',
130      '#component' => 'display_builder:library_panel',
131      '#slots' => [
132        'content' => $this->buildDraggables($builder_id, $build),
133      ],
134    ];
135  }
BlockLibraryPanel->buildCategorySection
148  private function buildCategorySection(array $category_data, string $builder_id): array {
149    $section = [];
150
151    if (!empty($category_data['label'])) {
153        '#type' => 'html_tag',
154        '#tag' => 'h4',
155        '#attributes' => ['class' => 'db-filter-hide-on-search'],
156        '#value' => $category_data['label'],
157      ];
158    }
159
160    foreach ($category_data['choices'] as $choice) {
160    foreach ($category_data['choices'] as $choice) {
160    foreach ($category_data['choices'] as $choice) {
161      $section[] = $choice['preview']
162        ? $this->buildPlaceholderButtonWithPreview($builder_id, $choice['label'], $choice['data'] ?? [], $choice['preview'], $choice['keywords'] ?? '')
161      $section[] = $choice['preview']
162        ? $this->buildPlaceholderButtonWithPreview($builder_id, $choice['label'], $choice['data'] ?? [], $choice['preview'], $choice['keywords'] ?? '')
163        : $this->buildPlaceholderButton($choice['label'], $choice['data'] ?? [], $choice['keywords'] ?? '');
160    foreach ($category_data['choices'] as $choice) {
161      $section[] = $choice['preview']
160    foreach ($category_data['choices'] as $choice) {
161      $section[] = $choice['preview']
162        ? $this->buildPlaceholderButtonWithPreview($builder_id, $choice['label'], $choice['data'] ?? [], $choice['preview'], $choice['keywords'] ?? '')
163        : $this->buildPlaceholderButton($choice['label'], $choice['data'] ?? [], $choice['keywords'] ?? '');
164    }
165
166    return $section;
167  }
BlockLibraryPanel->buildConfigurationForm
80  public function buildConfigurationForm(array $form, FormStateInterface $form_state): array {
81    $configuration = $this->getConfiguration();
82
83    $form['exclude'] = [
84      '#type' => 'checkboxes',
85      '#title' => $this->t('Exclude modules'),
86      '#options' => $this->getProvidersOptions(),
87      '#default_value' => $configuration['exclude'],
88    ];
89
90    return $form;
91  }
BlockLibraryPanel->configurationSummary
97    $configuration = $this->getConfiguration();
98
99    return [
100      $this->t('Excluded modules: @exclude', [
101        '@exclude' => \implode(', ', \array_filter($configuration['exclude'] ?? []) ?: [$this->t('None')]),
102      ]),
103    ];
104  }
BlockLibraryPanel->create
57  public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition): static {
58    $instance = parent::create($container, $configuration, $plugin_id, $plugin_definition);
59    $instance->moduleList = $container->get('extension.list.module');
60
61    return $instance;
62  }
BlockLibraryPanel->defaultConfiguration
70        'devel',
71        'htmx',
72        'shortcut',
73      ],
74    ];
75  }
BlockLibraryPanel->getProviders
248    $sources = $this->getSources();
249    $providers = [];
250    $modules = $this->moduleList->getAllInstalledInfo();
251
252    foreach ($sources as $source_data) {
252    foreach ($sources as $source_data) {
253      if (!isset($source_data['choices'])) {
254        continue;
256      $choices = $source_data['choices'];
257
258      foreach ($choices as $choice) {
258      foreach ($choices as $choice) {
259        $provider = $choice['provider'] ?? '';
260
261        if (!$provider || \in_array($provider, self::HIDE_PROVIDER, TRUE)) {
261        if (!$provider || \in_array($provider, self::HIDE_PROVIDER, TRUE)) {
261        if (!$provider || \in_array($provider, self::HIDE_PROVIDER, TRUE)) {
262          continue;
265        if (!isset($modules[$provider])) {
267          continue;
270        if (!isset($providers[$provider])) {
271          $providers[$provider] = $modules[$provider];
272          $providers[$provider]['count'] = 0;
273        }
274        ++$providers[$provider]['count'];
258      foreach ($choices as $choice) {
259        $provider = $choice['provider'] ?? '';
260
261        if (!$provider || \in_array($provider, self::HIDE_PROVIDER, TRUE)) {
262          continue;
263        }
264
265        if (!isset($modules[$provider])) {
266          // If the provider is not a module, skip it.
267          continue;
268        }
269
270        if (!isset($providers[$provider])) {
271          $providers[$provider] = $modules[$provider];
272          $providers[$provider]['count'] = 0;
273        }
274        ++$providers[$provider]['count'];
252    foreach ($sources as $source_data) {
253      if (!isset($source_data['choices'])) {
254        continue;
255      }
256      $choices = $source_data['choices'];
257
258      foreach ($choices as $choice) {
252    foreach ($sources as $source_data) {
253      if (!isset($source_data['choices'])) {
254        continue;
255      }
256      $choices = $source_data['choices'];
257
258      foreach ($choices as $choice) {
259        $provider = $choice['provider'] ?? '';
260
261        if (!$provider || \in_array($provider, self::HIDE_PROVIDER, TRUE)) {
262          continue;
263        }
264
265        if (!isset($modules[$provider])) {
266          // If the provider is not a module, skip it.
267          continue;
268        }
269
270        if (!isset($providers[$provider])) {
271          $providers[$provider] = $modules[$provider];
272          $providers[$provider]['count'] = 0;
273        }
274        ++$providers[$provider]['count'];
275      }
276    }
277
278    return $providers;
279  }
BlockLibraryPanel->getProvidersOptions
228    $options = [];
229
230    foreach ($this->getProviders() as $provider_id => $provider) {
230    foreach ($this->getProviders() as $provider_id => $provider) {
230    foreach ($this->getProviders() as $provider_id => $provider) {
230    foreach ($this->getProviders() as $provider_id => $provider) {
231      $params = [
232        '@name' => $provider['name'],
233        '@count' => $provider['count'],
234      ];
235      $options[$provider_id] = $this->formatPlural($provider['count'], '@name (@count block)', '@name (@count blocks)', $params);
236    }
237
238    return $options;
239  }
BlockLibraryPanel->getSources
178    if (!empty($this->sources)) {
179      return $this->sources;
182    $definitions = $this->sourceManager->getDefinitionsForPropType('slot', $this->configuration['contexts'] ?? []);
183    $slot_definition = ['ui_patterns' => ['type_definition' => $this->sourceManager->getSlotPropType()]];
184
185    foreach ($definitions as $source_id => $definition) {
185    foreach ($definitions as $source_id => $definition) {
185    foreach ($definitions as $source_id => $definition) {
186      // A block is a source for slots but without slots.
187      if (\is_a($definition['class'], SourceWithSlotsInterface::class, TRUE)) {
188        continue;
191      if (\in_array($source_id, self::HIDE_SOURCE, TRUE)) {
192        continue;
195      try {
196        $source = $this->sourceManager->createInstance(
201      catch (\Throwable $e) {
202        $this->logger->error('Invalid source found: %message', ['%message' => $e->getMessage()]);
203
204        continue;
208        'definition' => $definition,
209        'source' => $source,
210      ];
211
212      if ($source instanceof SourceWithChoicesInterface) {
185    foreach ($definitions as $source_id => $definition) {
186      // A block is a source for slots but without slots.
187      if (\is_a($definition['class'], SourceWithSlotsInterface::class, TRUE)) {
188        continue;
189      }
190
191      if (\in_array($source_id, self::HIDE_SOURCE, TRUE)) {
192        continue;
193      }
194
195      try {
196        $source = $this->sourceManager->createInstance(
197          $source_id,
198          SourcePluginBase::buildConfiguration('slot', $slot_definition, ['source' => []], $this->configuration['contexts'] ?? [])
199        );
200      }
201      catch (\Throwable $e) {
202        $this->logger->error('Invalid source found: %message', ['%message' => $e->getMessage()]);
203
204        continue;
205      }
206
207      $this->sources[$source_id] = [
208        'definition' => $definition,
209        'source' => $source,
210      ];
211
212      if ($source instanceof SourceWithChoicesInterface) {
213        $this->sources[$source_id]['choices'] = $source->getChoices();
185    foreach ($definitions as $source_id => $definition) {
185    foreach ($definitions as $source_id => $definition) {
186      // A block is a source for slots but without slots.
187      if (\is_a($definition['class'], SourceWithSlotsInterface::class, TRUE)) {
188        continue;
189      }
190
191      if (\in_array($source_id, self::HIDE_SOURCE, TRUE)) {
192        continue;
193      }
194
195      try {
196        $source = $this->sourceManager->createInstance(
197          $source_id,
198          SourcePluginBase::buildConfiguration('slot', $slot_definition, ['source' => []], $this->configuration['contexts'] ?? [])
199        );
200      }
201      catch (\Throwable $e) {
202        $this->logger->error('Invalid source found: %message', ['%message' => $e->getMessage()]);
203
204        continue;
205      }
206
207      $this->sources[$source_id] = [
208        'definition' => $definition,
209        'source' => $source,
210      ];
211
212      if ($source instanceof SourceWithChoicesInterface) {
213        $this->sources[$source_id]['choices'] = $source->getChoices();
214      }
215    }
216
217    return $this->sources;
218  }