Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
| Total | |
89.09% |
98 / 110 |
|
83.33% |
20 / 24 |
CRAP | |
0.00% |
0 / 1 |
| SqlMetadataRepository | |
89.91% |
98 / 109 |
|
83.33% |
20 / 24 |
37.33 | |
0.00% |
0 / 1 |
| __construct | |
100.00% |
3 / 3 |
|
100.00% |
1 / 1 |
1 | |||
| findModule | |
100.00% |
8 / 8 |
|
100.00% |
1 / 1 |
2 | |||
| findModuleById | |
87.50% |
7 / 8 |
|
0.00% |
0 / 1 |
2.01 | |||
| findAllActiveModules | |
100.00% |
9 / 9 |
|
100.00% |
1 / 1 |
3 | |||
| findFields | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
| findGlobalSearchFields | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
| findSections | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
| findFilter | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
| findModuleFilters | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
| findAllModuleOptions | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
| findFacetedFilterOptions | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
| findRelationsBySourceModule | |
100.00% |
9 / 9 |
|
100.00% |
1 / 1 |
2 | |||
| findMmRelationsBySourceModule | |
100.00% |
9 / 9 |
|
100.00% |
1 / 1 |
2 | |||
| findAllUserOptions | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
| findAllStructureOptions | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
| findAllPicklistOptions | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
| findAllUitypeOptions | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
| findFilterGrid | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
| findModuleFiltersGrid | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
| findWidget | |
100.00% |
6 / 6 |
|
100.00% |
1 / 1 |
2 | |||
| findAllActiveWidgets | |
0.00% |
0 / 8 |
|
0.00% |
0 / 1 |
12 | |||
| findGridRelationsByModule | |
100.00% |
34 / 34 |
|
100.00% |
1 / 1 |
4 | |||
| findPicklistValues | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
| clearCache | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
| 1 | <?php |
| 2 | |
| 3 | declare(strict_types=1); |
| 4 | |
| 5 | /** @license For full copyright and license information, please see the LICENSE.md file. */ |
| 6 | |
| 7 | namespace App\Core\Engine\Infrastructure\Repository; |
| 8 | |
| 9 | defined('AMMONLY_APP') || exit('Direct script access is forbidden.'); |
| 10 | |
| 11 | use App\Core\Engine\Domain\Exception\ModuleNotFoundException; |
| 12 | use App\Core\Engine\Domain\Model\FieldMetadata; |
| 13 | use App\Core\Engine\Domain\Model\FilterGridMetadata; |
| 14 | use App\Core\Engine\Domain\Model\FilterMetadata; |
| 15 | use App\Core\Engine\Domain\Model\ModuleMetadata; |
| 16 | use App\Core\Engine\Domain\Model\PermissionContext; |
| 17 | use App\Core\Engine\Domain\Model\Relation1mMetadata; |
| 18 | use App\Core\Engine\Domain\Model\RelationGridMetadata; |
| 19 | use App\Core\Engine\Domain\Model\RelationMmMetadata; |
| 20 | use App\Core\Engine\Domain\Model\SectionMetadata; |
| 21 | use App\Core\Engine\Domain\Model\WidgetMetadata; |
| 22 | use App\Core\Engine\Domain\Repository\MetadataRepositoryInterface; |
| 23 | use App\Core\Engine\Infrastructure\Repository\Loader\SqlFieldMetadataLoader; |
| 24 | use App\Core\Engine\Infrastructure\Repository\Loader\SqlFilterMetadataLoader; |
| 25 | use PDO; |
| 26 | |
| 27 | /** |
| 28 | * SQL Metadata Repository. |
| 29 | * |
| 30 | * Concrete PDO implementation of MetadataRepositoryInterface. |
| 31 | * Reads module, field, and filter metadata from the database. |
| 32 | * |
| 33 | * @package App\Core\Engine\Infrastructure\Repository |
| 34 | */ |
| 35 | final readonly class SqlMetadataRepository implements MetadataRepositoryInterface // NOSONAR |
| 36 | { |
| 37 | private SqlFacetedOptionsResolver $optionsResolver; |
| 38 | private SqlFilterMetadataLoader $filterLoader; |
| 39 | private SqlFieldMetadataLoader $fieldLoader; |
| 40 | |
| 41 | /** |
| 42 | * SqlMetadataRepository constructor. |
| 43 | * |
| 44 | * @param PDO $pdo Database connection. |
| 45 | * @param SqlFacetedOptionsResolver|null $optionsResolver Optional options resolver. |
| 46 | * @param SqlFilterMetadataLoader|null $filterLoader Optional filter loader. |
| 47 | * @param SqlFieldMetadataLoader|null $fieldLoader Optional field loader. |
| 48 | * @param string $tablePrefix Database table prefix. |
| 49 | */ |
| 50 | public function __construct( |
| 51 | private PDO $pdo, |
| 52 | ?SqlFacetedOptionsResolver $optionsResolver = null, |
| 53 | ?SqlFilterMetadataLoader $filterLoader = null, |
| 54 | ?SqlFieldMetadataLoader $fieldLoader = null, |
| 55 | string $tablePrefix = 'a_' |
| 56 | ) { |
| 57 | $this->optionsResolver = $optionsResolver ?? new SqlFacetedOptionsResolver($pdo, $tablePrefix); |
| 58 | $this->filterLoader = $filterLoader ?? new SqlFilterMetadataLoader($pdo); |
| 59 | $this->fieldLoader = $fieldLoader ?? new SqlFieldMetadataLoader($pdo); |
| 60 | } |
| 61 | |
| 62 | /** {@inheritdoc} */ |
| 63 | public function findModule(string $moduleName): ModuleMetadata |
| 64 | { |
| 65 | $sql = 'SELECT `id`, `type`, `name`, `label`, `route_url`, `api_endpoint`, |
| 66 | `icon_class`, `version`, `is_active`, `has_list`, `has_grid`, `has_calendar`, |
| 67 | `has_kanban`, `kanban_field_name`, `has_gantt`, `has_inventory`, |
| 68 | `default_view`, `default_create_mode`, `create_presentation_mode`, |
| 69 | `description`, `table_name`, `table_alias`, `primary_key`, |
| 70 | `allowed_hosts` |
| 71 | FROM `a_core_module_records` |
| 72 | WHERE `name` = :name |
| 73 | AND `is_active` = 1 |
| 74 | LIMIT 1'; |
| 75 | |
| 76 | $stmt = $this->pdo->prepare($sql); |
| 77 | $stmt->execute([':name' => $moduleName]); |
| 78 | /** @var array<string, mixed>|false $row */ |
| 79 | $row = $stmt->fetch(PDO::FETCH_ASSOC); |
| 80 | |
| 81 | if ($row === false) { |
| 82 | throw ModuleNotFoundException::forName($moduleName); |
| 83 | } |
| 84 | |
| 85 | return ModuleMetadata::fromRow($row); |
| 86 | } |
| 87 | |
| 88 | /** {@inheritdoc} */ |
| 89 | public function findModuleById(int $moduleId): ModuleMetadata |
| 90 | { |
| 91 | $sql = 'SELECT `id`, `type`, `name`, `label`, `route_url`, `api_endpoint`, |
| 92 | `icon_class`, `version`, `is_active`, `has_list`, `has_grid`, `has_calendar`, |
| 93 | `has_kanban`, `kanban_field_name`, `has_gantt`, `has_inventory`, |
| 94 | `default_view`, `default_create_mode`, `create_presentation_mode`, |
| 95 | `description`, `table_name`, `table_alias`, `primary_key`, |
| 96 | `allowed_hosts` |
| 97 | FROM `a_core_module_records` |
| 98 | WHERE `id` = :id |
| 99 | AND `is_active` = 1 |
| 100 | LIMIT 1'; |
| 101 | |
| 102 | $stmt = $this->pdo->prepare($sql); |
| 103 | $stmt->execute([':id' => $moduleId]); |
| 104 | /** @var array<string, mixed>|false $row */ |
| 105 | $row = $stmt->fetch(PDO::FETCH_ASSOC); |
| 106 | |
| 107 | if ($row === false) { |
| 108 | throw ModuleNotFoundException::forName((string) $moduleId); |
| 109 | } |
| 110 | |
| 111 | return ModuleMetadata::fromRow($row); |
| 112 | } |
| 113 | |
| 114 | /** {@inheritdoc} */ |
| 115 | public function findAllActiveModules(): array |
| 116 | { |
| 117 | $sql = 'SELECT `id`, `type`, `name`, `label`, `route_url`, `api_endpoint`, |
| 118 | `icon_class`, `version`, `is_active`, `has_list`, `has_grid`, `has_calendar`, |
| 119 | `has_kanban`, `kanban_field_name`, `has_gantt`, `has_inventory`, |
| 120 | `default_view`, `default_create_mode`, `create_presentation_mode`, |
| 121 | `description`, `table_name`, `table_alias`, `primary_key`, |
| 122 | `allowed_hosts` |
| 123 | FROM `a_core_module_records` |
| 124 | WHERE `is_active` = 1 |
| 125 | ORDER BY `label` ASC'; |
| 126 | |
| 127 | $stmt = $this->pdo->query($sql); |
| 128 | /** @var array<int, array<string, mixed>> $rows */ |
| 129 | $rows = $stmt !== false ? $stmt->fetchAll(PDO::FETCH_ASSOC) : []; |
| 130 | |
| 131 | $modules = []; |
| 132 | foreach ($rows as $row) { |
| 133 | /** @var string $name */ |
| 134 | $name = $row['name']; |
| 135 | $modules[$name] = ModuleMetadata::fromRow($row); |
| 136 | } |
| 137 | |
| 138 | return $modules; |
| 139 | } |
| 140 | |
| 141 | /** {@inheritdoc} */ |
| 142 | public function findFields(int $moduleId): array |
| 143 | { |
| 144 | return $this->fieldLoader->findFields($moduleId); |
| 145 | } |
| 146 | |
| 147 | /** {@inheritdoc} */ |
| 148 | public function findGlobalSearchFields(int $moduleId): array |
| 149 | { |
| 150 | return $this->fieldLoader->findGlobalSearchFields($moduleId); |
| 151 | } |
| 152 | |
| 153 | /** {@inheritdoc} */ |
| 154 | public function findSections(int $moduleId): array |
| 155 | { |
| 156 | return $this->fieldLoader->findSections($moduleId); |
| 157 | } |
| 158 | |
| 159 | /** {@inheritdoc} */ |
| 160 | public function findFilter(int $moduleId, ?int $filterId): FilterMetadata |
| 161 | { |
| 162 | return $this->filterLoader->findFilter($moduleId, $filterId, $this); |
| 163 | } |
| 164 | |
| 165 | /** {@inheritdoc} */ |
| 166 | public function findModuleFilters(int $moduleId, PermissionContext $context): array |
| 167 | { |
| 168 | return $this->filterLoader->findModuleFilters($moduleId, $context, $this); |
| 169 | } |
| 170 | |
| 171 | /** {@inheritdoc} */ |
| 172 | public function findAllModuleOptions(): array |
| 173 | { |
| 174 | return $this->optionsResolver->findAllModuleOptions(); |
| 175 | } |
| 176 | |
| 177 | /** {@inheritdoc} */ |
| 178 | public function findFacetedFilterOptions( |
| 179 | ModuleMetadata $module, |
| 180 | array $fields, |
| 181 | PermissionContext $context |
| 182 | ): array { |
| 183 | return $this->optionsResolver->findFacetedFilterOptions($fields); |
| 184 | } |
| 185 | |
| 186 | /** {@inheritdoc} */ |
| 187 | public function findRelationsBySourceModule(int $sourceModuleId): array |
| 188 | { |
| 189 | $sql = 'SELECT r.`id`, r.`name`, r.`label`, r.`source_module_id`, r.`target_module_id`, |
| 190 | r.`target_field_key`, r.`relation_type`, r.`is_active`, r.`sort_order`, |
| 191 | r.`visible_fields`, r.`allowed_actions`, |
| 192 | sm.`name` AS `source_module_name`, |
| 193 | tm.`name` AS `target_module_name`, |
| 194 | tm.`route_url` AS `target_module_route`, |
| 195 | tm.`icon_class` AS `target_module_icon` |
| 196 | FROM `a_core_relation_1m_records` r |
| 197 | JOIN `a_core_module_records` sm ON sm.`id` = r.`source_module_id` |
| 198 | JOIN `a_core_module_records` tm ON tm.`id` = r.`target_module_id` |
| 199 | WHERE r.`source_module_id` = :source_module_id AND r.`is_active` = 1 |
| 200 | ORDER BY r.`sort_order` ASC, r.`id` ASC'; |
| 201 | |
| 202 | $stmt = $this->pdo->prepare($sql); |
| 203 | $stmt->execute([':source_module_id' => $sourceModuleId]); |
| 204 | /** @var array<int, array<string, mixed>> $rows */ |
| 205 | $rows = $stmt->fetchAll(PDO::FETCH_ASSOC); |
| 206 | |
| 207 | $relations = []; |
| 208 | foreach ($rows as $row) { |
| 209 | $relations[] = Relation1mMetadata::fromRow($row); |
| 210 | } |
| 211 | |
| 212 | return $relations; |
| 213 | } |
| 214 | |
| 215 | /** {@inheritdoc} */ |
| 216 | public function findMmRelationsBySourceModule(int $sourceModuleId): array |
| 217 | { |
| 218 | $sql = "SELECT r.`id`, r.`name`, r.`label`, r.`source_module_ids`, r.`target_module_ids`, |
| 219 | r.`intermediate_table`, r.`relation_type`, r.`view_scope`, r.`record_limit`, |
| 220 | r.`is_active`, r.`sort_order`, r.`visible_fields`, r.`allowed_actions`, |
| 221 | sm.`name` AS `source_module_name`, |
| 222 | tm.`name` AS `target_module_name`, |
| 223 | tm.`label` AS `target_module_label`, |
| 224 | tm.`route_url` AS `target_module_route`, |
| 225 | tm.`icon_class` AS `target_module_icon` |
| 226 | FROM `a_core_relation_mm_records` r |
| 227 | LEFT JOIN `a_core_module_records` sm |
| 228 | ON sm.`id` = CAST(JSON_UNQUOTE(JSON_EXTRACT(r.`source_module_ids`, '$[0]')) AS UNSIGNED) |
| 229 | LEFT JOIN `a_core_module_records` tm |
| 230 | ON tm.`id` = CAST(JSON_UNQUOTE(JSON_EXTRACT(r.`target_module_ids`, '$[0]')) AS UNSIGNED) |
| 231 | WHERE JSON_CONTAINS(r.`source_module_ids`, :source_module_id) |
| 232 | AND r.`is_active` = 1 |
| 233 | ORDER BY r.`sort_order` ASC, r.`id` ASC"; |
| 234 | |
| 235 | $stmt = $this->pdo->prepare($sql); |
| 236 | $stmt->execute([':source_module_id' => (string) json_encode($sourceModuleId)]); |
| 237 | /** @var array<int, array<string, mixed>> $rows */ |
| 238 | $rows = $stmt->fetchAll(PDO::FETCH_ASSOC); |
| 239 | |
| 240 | $relations = []; |
| 241 | foreach ($rows as $row) { |
| 242 | $relations[] = RelationMmMetadata::fromRow($row); |
| 243 | } |
| 244 | |
| 245 | return $relations; |
| 246 | } |
| 247 | |
| 248 | /** {@inheritdoc} */ |
| 249 | public function findAllUserOptions(?string $tablePrefix = null): array |
| 250 | { |
| 251 | return $this->optionsResolver->findAllUserOptions($tablePrefix); |
| 252 | } |
| 253 | |
| 254 | /** {@inheritdoc} */ |
| 255 | public function findAllStructureOptions(?string $tablePrefix = null): array |
| 256 | { |
| 257 | return $this->optionsResolver->findAllStructureOptions($tablePrefix); |
| 258 | } |
| 259 | |
| 260 | /** {@inheritdoc} */ |
| 261 | public function findAllPicklistOptions(?int $moduleId = null): array |
| 262 | { |
| 263 | return $this->optionsResolver->findAllPicklistOptions($moduleId); |
| 264 | } |
| 265 | |
| 266 | /** {@inheritdoc} */ |
| 267 | public function findAllUitypeOptions(): array |
| 268 | { |
| 269 | return $this->optionsResolver->findAllUitypeOptions(); |
| 270 | } |
| 271 | |
| 272 | /** {@inheritdoc} */ |
| 273 | public function findFilterGrid(int $moduleId, ?int $filterGridId): FilterGridMetadata |
| 274 | { |
| 275 | return $this->filterLoader->findFilterGrid($moduleId, $filterGridId); |
| 276 | } |
| 277 | |
| 278 | /** {@inheritdoc} */ |
| 279 | public function findModuleFiltersGrid(int $moduleId, PermissionContext $context): array |
| 280 | { |
| 281 | return $this->filterLoader->findModuleFiltersGrid($moduleId, $context); |
| 282 | } |
| 283 | |
| 284 | /** {@inheritdoc} */ |
| 285 | public function findWidget(int $widgetId): ?WidgetMetadata |
| 286 | { |
| 287 | $sql = 'SELECT `id`, `name`, `label`, `category`, `handler_class`, `default_w`, |
| 288 | `default_h`, `min_w`, `max_w`, `min_h`, `max_h`, `is_resizable`, |
| 289 | `is_movable`, `is_active`, `is_system`, `description`, `default_params` |
| 290 | FROM `a_core_widget_records` |
| 291 | WHERE `id` = :id |
| 292 | AND `is_active` = 1 |
| 293 | LIMIT 1'; |
| 294 | |
| 295 | $stmt = $this->pdo->prepare($sql); |
| 296 | $stmt->execute([':id' => $widgetId]); |
| 297 | /** @var array<string, mixed>|false $row */ |
| 298 | $row = $stmt->fetch(PDO::FETCH_ASSOC); |
| 299 | |
| 300 | return $row !== false ? WidgetMetadata::fromRow($row) : null; |
| 301 | } |
| 302 | |
| 303 | /** {@inheritdoc} */ |
| 304 | public function findAllActiveWidgets(): array |
| 305 | { |
| 306 | $sql = 'SELECT `id`, `name`, `label`, `category`, `handler_class`, `default_w`, |
| 307 | `default_h`, `min_w`, `max_w`, `min_h`, `max_h`, `is_resizable`, |
| 308 | `is_movable`, `is_active`, `is_system`, `description`, `default_params` |
| 309 | FROM `a_core_widget_records` |
| 310 | WHERE `is_active` = 1 |
| 311 | ORDER BY `category` ASC, `label` ASC, `id` ASC'; |
| 312 | |
| 313 | $stmt = $this->pdo->query($sql); |
| 314 | /** @var array<int, array<string, mixed>> $rows */ |
| 315 | $rows = $stmt !== false ? $stmt->fetchAll(PDO::FETCH_ASSOC) : []; |
| 316 | $widgets = []; |
| 317 | |
| 318 | foreach ($rows as $row) { |
| 319 | $widgets[] = WidgetMetadata::fromRow($row); |
| 320 | } |
| 321 | |
| 322 | return $widgets; |
| 323 | } |
| 324 | |
| 325 | /** {@inheritdoc} */ |
| 326 | public function findGridRelationsByModule(int $moduleId, ?int $filterGridId = null): array |
| 327 | { |
| 328 | $sql = 'SELECT rg.`id`, rg.`name`, rg.`label`, rg.`module_id`, rg.`grid_filter_id`, |
| 329 | rg.`widget_id`, rg.`pos_x`, rg.`pos_y`, rg.`width`, rg.`height`, |
| 330 | rg.`is_locked`, rg.`widget_params`, rg.`is_active`, rg.`sort_order`, |
| 331 | w.`id` AS `w_id`, w.`name` AS `w_name`, w.`label` AS `w_label`, |
| 332 | w.`category` AS `w_category`, w.`handler_class` AS `w_handler_class`, |
| 333 | w.`default_w` AS `w_default_w`, w.`default_h` AS `w_default_h`, |
| 334 | w.`min_w` AS `w_min_w`, w.`max_w` AS `w_max_w`, |
| 335 | w.`min_h` AS `w_min_h`, w.`max_h` AS `w_max_h`, |
| 336 | w.`is_resizable` AS `w_is_resizable`, w.`is_movable` AS `w_is_movable`, |
| 337 | w.`is_active` AS `w_is_active`, w.`is_system` AS `w_is_system`, |
| 338 | w.`description` AS `w_description`, w.`default_params` AS `w_default_params` |
| 339 | FROM `a_core_relation_grid_records` rg |
| 340 | JOIN `a_core_widget_records` w ON w.`id` = rg.`widget_id` |
| 341 | WHERE rg.`module_id` = :module_id |
| 342 | AND rg.`is_active` = 1 |
| 343 | AND w.`is_active` = 1'; |
| 344 | |
| 345 | $params = [':module_id' => $moduleId]; |
| 346 | if ($filterGridId !== null && $filterGridId > 0) { |
| 347 | $sql .= ' AND (rg.`grid_filter_id` = :grid_filter_id OR rg.`grid_filter_id` IS NULL)'; |
| 348 | $params[':grid_filter_id'] = $filterGridId; |
| 349 | } |
| 350 | |
| 351 | $sql .= ' ORDER BY rg.`sort_order` ASC, rg.`pos_y` ASC, rg.`pos_x` ASC, rg.`id` ASC'; |
| 352 | |
| 353 | $stmt = $this->pdo->prepare($sql); |
| 354 | $stmt->execute($params); |
| 355 | /** @var array<int, array<string, mixed>> $rows */ |
| 356 | $rows = $stmt->fetchAll(PDO::FETCH_ASSOC); |
| 357 | $relations = []; |
| 358 | |
| 359 | foreach ($rows as $row) { |
| 360 | $widgetRow = [ |
| 361 | 'id' => $row['w_id'] ?? $row['widget_id'] ?? 0, |
| 362 | 'name' => $row['w_name'] ?? $row['widget_name'] ?? '', |
| 363 | 'label' => $row['w_label'] ?? $row['widget_label'] ?? '', |
| 364 | 'category' => $row['w_category'] ?? $row['widget_category'] ?? 'general', |
| 365 | 'handler_class' => $row['w_handler_class'] ?? null, |
| 366 | 'default_w' => $row['w_default_w'] ?? 6, |
| 367 | 'default_h' => $row['w_default_h'] ?? 4, |
| 368 | 'min_w' => $row['w_min_w'] ?? 2, |
| 369 | 'max_w' => $row['w_max_w'] ?? 12, |
| 370 | 'min_h' => $row['w_min_h'] ?? 2, |
| 371 | 'max_h' => $row['w_max_h'] ?? 12, |
| 372 | 'is_resizable' => $row['w_is_resizable'] ?? 1, |
| 373 | 'is_movable' => $row['w_is_movable'] ?? 1, |
| 374 | 'is_active' => $row['w_is_active'] ?? 1, |
| 375 | 'is_system' => $row['w_is_system'] ?? 0, |
| 376 | 'description' => $row['w_description'] ?? null, |
| 377 | 'default_params' => $row['w_default_params'] ?? null, |
| 378 | ]; |
| 379 | $widget = WidgetMetadata::fromRow($widgetRow); |
| 380 | $relations[] = RelationGridMetadata::fromRow($row, $widget); |
| 381 | } |
| 382 | |
| 383 | return $relations; |
| 384 | } |
| 385 | |
| 386 | /** {@inheritdoc} */ |
| 387 | public function findPicklistValues(int $picklistId): array |
| 388 | { |
| 389 | return $this->optionsResolver->findPicklistValues($picklistId); |
| 390 | } |
| 391 | |
| 392 | /** {@inheritdoc} */ |
| 393 | public function clearCache(?int $moduleId = null): void |
| 394 | { |
| 395 | // Concrete SQL repository operates without persistent state. |
| 396 | } |
| 397 | } |