Code Coverage |
||||||||||||||||
Lines |
Branches |
Paths |
Functions and Methods |
Classes and Traits |
||||||||||||
| Total | |
69.86% |
51 / 73 |
|
67.35% |
33 / 49 |
|
18.57% |
13 / 70 |
|
60.00% |
3 / 5 |
CRAP | |
0.00% |
0 / 1 |
| ComponentLibraryDefinitionHelper | |
69.86% |
51 / 73 |
|
67.35% |
33 / 49 |
|
18.57% |
13 / 70 |
|
60.00% |
3 / 5 |
515.93 | |
0.00% |
0 / 1 |
| __construct | |
100.00% |
2 / 2 |
|
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
| getDefinitions | |
100.00% |
24 / 24 |
|
100.00% |
8 / 8 |
|
40.00% |
2 / 5 |
|
100.00% |
1 / 1 |
10.40 | |||
| prepareComponentData | |
32.14% |
9 / 28 |
|
29.41% |
5 / 17 |
|
15.38% |
2 / 13 |
|
0.00% |
0 / 1 |
46.77 | |||
| filterAccordingToConfiguration | |
100.00% |
11 / 11 |
|
100.00% |
14 / 14 |
|
16.28% |
7 / 43 |
|
100.00% |
1 / 1 |
68.68 | |||
| getDefaultValue | |
62.50% |
5 / 8 |
|
55.56% |
5 / 9 |
|
12.50% |
1 / 8 |
|
0.00% |
0 / 1 |
30.12 | |||
| 1 | <?php |
| 2 | |
| 3 | declare(strict_types=1); |
| 4 | |
| 5 | namespace Drupal\display_builder; |
| 6 | |
| 7 | use Drupal\Component\Plugin\Definition\PluginDefinitionInterface; |
| 8 | use Drupal\Core\Plugin\Component; |
| 9 | use Drupal\Core\Theme\ComponentPluginManager; |
| 10 | use Drupal\ui_patterns\SourcePluginManager; |
| 11 | use Drupal\ui_patterns\SourceWithChoicesInterface; |
| 12 | |
| 13 | /** |
| 14 | * Helper class for block library source handling. |
| 15 | */ |
| 16 | class ComponentLibraryDefinitionHelper { |
| 17 | |
| 18 | /** |
| 19 | * The UI Patterns source plugin manager. |
| 20 | */ |
| 21 | protected SourcePluginManager $sourceManager; |
| 22 | |
| 23 | /** |
| 24 | * The UI Patterns source plugin manager. |
| 25 | */ |
| 26 | private ComponentPluginManager $sdcManager; |
| 27 | |
| 28 | public function __construct(ComponentPluginManager $sdcManager, SourcePluginManager $sourceManager) { |
| 29 | $this->sdcManager = $sdcManager; |
| 30 | $this->sourceManager = $sourceManager; |
| 31 | } |
| 32 | |
| 33 | /** |
| 34 | * Init filtered and grouped definitions, and source data. |
| 35 | * |
| 36 | * @param array $configuration |
| 37 | * Component library panel definition. |
| 38 | * |
| 39 | * @return array |
| 40 | * An associative array of arrays, where each sub array is keyed by |
| 41 | * component ID. |
| 42 | */ |
| 43 | public function getDefinitions(array $configuration): array { |
| 44 | /** @var \Drupal\ui_patterns\ComponentPluginManager $uiPatternsSdcManager */ |
| 45 | $uiPatternsSdcManager = $this->sdcManager; |
| 46 | $definitions = $uiPatternsSdcManager->getNegotiatedSortedDefinitions(); |
| 47 | $filtered = $grouped = $sources = []; |
| 48 | $exclude_by_id = \preg_split('/\s+/', \trim($configuration['exclude_id'] ?? '')) ?: []; |
| 49 | /** @var \Drupal\ui_patterns\SourceWithChoicesInterface $source */ |
| 50 | $source = $this->sourceManager->createInstance('component'); |
| 51 | |
| 52 | foreach ($definitions as $id => $definition) { |
| 53 | if (!self::filterAccordingToConfiguration($id, $definition, $configuration, $exclude_by_id)) { |
| 54 | continue; |
| 55 | } |
| 56 | |
| 57 | $component = $this->sdcManager->find($id); |
| 58 | $data = $this->prepareComponentData($source, $component); |
| 59 | |
| 60 | if ($data === NULL) { |
| 61 | // We skip components with at least a required prop without default |
| 62 | // value in order to avoid \Twig\Error\RuntimeError. |
| 63 | // In UI Patterns 2, we rely on Form API blocking ComponentForm |
| 64 | // submission to avoid this unfortunate situation. |
| 65 | // In Display Builder, we can render component without submitting |
| 66 | // ComponentForm, on preview and builder panels for example. |
| 67 | // @todo Do we send a log message? |
| 68 | continue; |
| 69 | } |
| 70 | |
| 71 | $sources[$id] = $data; |
| 72 | $filtered[$id] = $definition; |
| 73 | $grouped[(string) $definition['category']][$id] = $definition; |
| 74 | } |
| 75 | |
| 76 | // Order list ignoring starting '(' that is used for components names that |
| 77 | // are sub components. |
| 78 | \uasort($filtered, static function ($a, $b) { |
| 79 | $nameA = \ltrim($a['name'] ?? $a['label'], '('); |
| 80 | |
| 81 | return \strnatcasecmp($nameA, \ltrim($b['name'] ?? $b['label'], '(')); |
| 82 | }); |
| 83 | |
| 84 | return [ |
| 85 | 'filtered' => $filtered, |
| 86 | 'grouped' => $grouped, |
| 87 | 'sources' => $sources, |
| 88 | ]; |
| 89 | } |
| 90 | |
| 91 | /** |
| 92 | * Prepare component data. |
| 93 | * |
| 94 | * @param \Drupal\ui_patterns\SourceWithChoicesInterface $source |
| 95 | * Component source. |
| 96 | * @param \Drupal\Core\Plugin\Component $component |
| 97 | * Component. |
| 98 | * |
| 99 | * @return ?array |
| 100 | * The component data, with the default value for required props. |
| 101 | * If NULL, that means a required prop has no default value and the |
| 102 | * component will be skipped. |
| 103 | */ |
| 104 | private function prepareComponentData(SourceWithChoicesInterface $source, Component $component): ?array { |
| 105 | $data = $source->getChoiceSettings($component->getPluginId()); |
| 106 | $props = $component->metadata->schema['properties'] ?? []; |
| 107 | $source_cache = []; |
| 108 | |
| 109 | foreach ($component->metadata->schema['required'] ?? [] as $prop_id) { |
| 110 | $prop = $props[$prop_id]; |
| 111 | |
| 112 | $default = self::getDefaultValue($prop); |
| 113 | |
| 114 | if ($default === NULL) { |
| 115 | return NULL; |
| 116 | } |
| 117 | $prop_type = $prop['ui_patterns']['type_definition'] ?? NULL; |
| 118 | |
| 119 | if (!$prop_type) { |
| 120 | return NULL; |
| 121 | } |
| 122 | $prop_source_id = $prop_type->getDefaultSourceId(); |
| 123 | |
| 124 | if (!$prop_source_id) { |
| 125 | return NULL; |
| 126 | } |
| 127 | |
| 128 | if (!isset($source_cache[$prop_source_id])) { |
| 129 | /** @var \Drupal\ui_patterns\SourceInterface $default_source */ |
| 130 | $source_cache[$prop_source_id] = $this->sourceManager->createInstance($prop_source_id); |
| 131 | } |
| 132 | $default_source = $source_cache[$prop_source_id]; |
| 133 | |
| 134 | // We bet on widgets having a 'value' configuration. It may be a bit |
| 135 | // naive, but the most important is to avoid \Twig\Error\RuntimeError. |
| 136 | $definition = $default_source->getPluginDefinition(); |
| 137 | $tags = ($definition instanceof PluginDefinitionInterface) ? [] : $definition['tags'] ?? []; |
| 138 | |
| 139 | if (!\in_array('widget', $tags, TRUE)) { |
| 140 | return NULL; |
| 141 | } |
| 142 | $data['component']['props'][$prop_id] = [ |
| 143 | 'source_id' => $default_source->getPluginId(), |
| 144 | 'source' => [ |
| 145 | 'value' => $default, |
| 146 | ], |
| 147 | ]; |
| 148 | } |
| 149 | |
| 150 | return $data; |
| 151 | } |
| 152 | |
| 153 | /** |
| 154 | * Filter definitions according to configuration. |
| 155 | * |
| 156 | * @param string $id |
| 157 | * Component ID, the array key. |
| 158 | * @param array $definition |
| 159 | * Component definition, the array value. |
| 160 | * @param array $configuration |
| 161 | * Component library panel configuration. |
| 162 | * @param string[] $exclude_by_id |
| 163 | * An array of component IDs to exclude. |
| 164 | * |
| 165 | * @return bool |
| 166 | * Must the component be kept? |
| 167 | */ |
| 168 | private static function filterAccordingToConfiguration(string $id, array $definition, array $configuration, array $exclude_by_id): bool { |
| 169 | if (isset($definition['provider']) && \in_array($id, $exclude_by_id, TRUE)) { |
| 170 | return FALSE; |
| 171 | } |
| 172 | |
| 173 | if (isset($definition['provider']) && \in_array($definition['provider'], $configuration['exclude'], TRUE)) { |
| 174 | return FALSE; |
| 175 | } |
| 176 | |
| 177 | // Excluded no ui components unless forced. |
| 178 | if (isset($definition['noUi']) && $definition['noUi'] === TRUE) { |
| 179 | if ((bool) $configuration['include_no_ui'] !== TRUE) { |
| 180 | return FALSE; |
| 181 | } |
| 182 | } |
| 183 | |
| 184 | // Filter components according to configuration. |
| 185 | // Components with stable or undefined status will always be available. |
| 186 | $allowed_status = \array_merge($configuration['component_status'], ['stable']); |
| 187 | |
| 188 | if (isset($definition['status']) && !\in_array($definition['status'], $allowed_status, TRUE)) { |
| 189 | return FALSE; |
| 190 | } |
| 191 | |
| 192 | return TRUE; |
| 193 | } |
| 194 | |
| 195 | /** |
| 196 | * Get default value for prop. |
| 197 | * |
| 198 | * @param array $prop |
| 199 | * Prop JSON schema definition. |
| 200 | * |
| 201 | * @return mixed |
| 202 | * NULL if no default value found. |
| 203 | */ |
| 204 | private static function getDefaultValue(array $prop): mixed { |
| 205 | // First, we try to get teh default value or the first example. |
| 206 | $default = $prop['default'] ?? $prop['examples'][0] ?? NULL; |
| 207 | |
| 208 | if ($default !== NULL) { |
| 209 | return $default; |
| 210 | } |
| 211 | |
| 212 | // Then, we try to get the first value from enumeration. |
| 213 | if (isset($prop['enum']) && !empty($prop['enum'])) { |
| 214 | return $prop['enum'][0]; |
| 215 | } |
| 216 | |
| 217 | // Finally, we set the boolean value to false. Boolean is the only JSON |
| 218 | // schema type where we can be sure the default value is OK because there is |
| 219 | // no additional criteria to deal with: |
| 220 | // - string has minLength, maxLength, pattern... |
| 221 | // - array has items, minItems, maxItems, uniqueItems... |
| 222 | // - object has properties... |
| 223 | // - number and integer have multipleOf, minimum, maximum... |
| 224 | // There is this weird mechanism in SDC adding the object type to all |
| 225 | // props. We need to deal with that until we remove it. |
| 226 | // @see \Drupal\Core\Theme\Component\ComponentMetadata::parseSchemaInfo() |
| 227 | if ($prop['type'] === 'boolean' || empty(\array_diff($prop['type'], ['object', 'boolean']))) { |
| 228 | return FALSE; |
| 229 | } |
| 230 | |
| 231 | return NULL; |
| 232 | } |
| 233 | |
| 234 | } |