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 | } |
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.
| 28 | public function __construct(ComponentPluginManager $sdcManager, SourcePluginManager $sourceManager) { |
| 29 | $this->sdcManager = $sdcManager; |
| 30 | $this->sourceManager = $sourceManager; |
| 31 | } |
| 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)) { |
| 169 | if (isset($definition['provider']) && \in_array($id, $exclude_by_id, TRUE)) { |
| 170 | return FALSE; |
| 173 | if (isset($definition['provider']) && \in_array($definition['provider'], $configuration['exclude'], TRUE)) { |
| 173 | if (isset($definition['provider']) && \in_array($definition['provider'], $configuration['exclude'], TRUE)) { |
| 174 | return FALSE; |
| 178 | if (isset($definition['noUi']) && $definition['noUi'] === TRUE) { |
| 178 | if (isset($definition['noUi']) && $definition['noUi'] === TRUE) { |
| 179 | if ((bool) $configuration['include_no_ui'] !== TRUE) { |
| 180 | return FALSE; |
| 186 | $allowed_status = \array_merge($configuration['component_status'], ['stable']); |
| 187 | |
| 188 | if (isset($definition['status']) && !\in_array($definition['status'], $allowed_status, TRUE)) { |
| 188 | if (isset($definition['status']) && !\in_array($definition['status'], $allowed_status, TRUE)) { |
| 189 | return FALSE; |
| 192 | return TRUE; |
| 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; |
| 213 | if (isset($prop['enum']) && !empty($prop['enum'])) { |
| 213 | if (isset($prop['enum']) && !empty($prop['enum'])) { |
| 214 | return $prop['enum'][0]; |
| 227 | if ($prop['type'] === 'boolean' || empty(\array_diff($prop['type'], ['object', 'boolean']))) { |
| 227 | if ($prop['type'] === 'boolean' || empty(\array_diff($prop['type'], ['object', 'boolean']))) { |
| 228 | return FALSE; |
| 231 | return NULL; |
| 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) { |
| 52 | foreach ($definitions as $id => $definition) { |
| 52 | foreach ($definitions as $id => $definition) { |
| 53 | if (!self::filterAccordingToConfiguration($id, $definition, $configuration, $exclude_by_id)) { |
| 54 | continue; |
| 57 | $component = $this->sdcManager->find($id); |
| 58 | $data = $this->prepareComponentData($source, $component); |
| 59 | |
| 60 | if ($data === NULL) { |
| 68 | continue; |
| 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; |
| 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, |
| 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) { |
| 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; |
| 117 | $prop_type = $prop['ui_patterns']['type_definition'] ?? NULL; |
| 118 | |
| 119 | if (!$prop_type) { |
| 120 | return NULL; |
| 122 | $prop_source_id = $prop_type->getDefaultSourceId(); |
| 123 | |
| 124 | if (!$prop_source_id) { |
| 125 | return NULL; |
| 128 | if (!isset($source_cache[$prop_source_id])) { |
| 130 | $source_cache[$prop_source_id] = $this->sourceManager->createInstance($prop_source_id); |
| 131 | } |
| 132 | $default_source = $source_cache[$prop_source_id]; |
| 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'] ?? []; |
| 137 | $tags = ($definition instanceof PluginDefinitionInterface) ? [] : $definition['tags'] ?? []; |
| 137 | $tags = ($definition instanceof PluginDefinitionInterface) ? [] : $definition['tags'] ?? []; |
| 137 | $tags = ($definition instanceof PluginDefinitionInterface) ? [] : $definition['tags'] ?? []; |
| 138 | |
| 139 | if (!\in_array('widget', $tags, TRUE)) { |
| 140 | return NULL; |
| 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(), |
| 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; |
| 78 | \uasort($filtered, static function ($a, $b) { |
| 79 | $nameA = \ltrim($a['name'] ?? $a['label'], '('); |
| 80 | |
| 81 | return \strnatcasecmp($nameA, \ltrim($b['name'] ?? $b['label'], '(')); |