Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
89.09% covered (warning)
89.09%
98 / 110
83.33% covered (warning)
83.33%
20 / 24
CRAP
0.00% covered (danger)
0.00%
0 / 1
SqlMetadataRepository
89.91% covered (warning)
89.91%
98 / 109
83.33% covered (warning)
83.33%
20 / 24
37.33
0.00% covered (danger)
0.00%
0 / 1
 __construct
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
1
 findModule
100.00% covered (success)
100.00%
8 / 8
100.00% covered (success)
100.00%
1 / 1
2
 findModuleById
87.50% covered (warning)
87.50%
7 / 8
0.00% covered (danger)
0.00%
0 / 1
2.01
 findAllActiveModules
100.00% covered (success)
100.00%
9 / 9
100.00% covered (success)
100.00%
1 / 1
3
 findFields
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 findGlobalSearchFields
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 findSections
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 findFilter
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 findModuleFilters
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 findAllModuleOptions
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 findFacetedFilterOptions
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 findRelationsBySourceModule
100.00% covered (success)
100.00%
9 / 9
100.00% covered (success)
100.00%
1 / 1
2
 findMmRelationsBySourceModule
100.00% covered (success)
100.00%
9 / 9
100.00% covered (success)
100.00%
1 / 1
2
 findAllUserOptions
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 findAllStructureOptions
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 findAllPicklistOptions
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 findAllUitypeOptions
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 findFilterGrid
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 findModuleFiltersGrid
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 findWidget
100.00% covered (success)
100.00%
6 / 6
100.00% covered (success)
100.00%
1 / 1
2
 findAllActiveWidgets
0.00% covered (danger)
0.00%
0 / 8
0.00% covered (danger)
0.00%
0 / 1
12
 findGridRelationsByModule
100.00% covered (success)
100.00%
34 / 34
100.00% covered (success)
100.00%
1 / 1
4
 findPicklistValues
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 clearCache
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
1<?php
2
3declare(strict_types=1);
4
5/** @license For full copyright and license information, please see the LICENSE.md file. */
6
7namespace App\Core\Engine\Infrastructure\Repository;
8
9defined('AMMONLY_APP') || exit('Direct script access is forbidden.');
10
11use App\Core\Engine\Domain\Exception\ModuleNotFoundException;
12use App\Core\Engine\Domain\Model\FieldMetadata;
13use App\Core\Engine\Domain\Model\FilterGridMetadata;
14use App\Core\Engine\Domain\Model\FilterMetadata;
15use App\Core\Engine\Domain\Model\ModuleMetadata;
16use App\Core\Engine\Domain\Model\PermissionContext;
17use App\Core\Engine\Domain\Model\Relation1mMetadata;
18use App\Core\Engine\Domain\Model\RelationGridMetadata;
19use App\Core\Engine\Domain\Model\RelationMmMetadata;
20use App\Core\Engine\Domain\Model\SectionMetadata;
21use App\Core\Engine\Domain\Model\WidgetMetadata;
22use App\Core\Engine\Domain\Repository\MetadataRepositoryInterface;
23use App\Core\Engine\Infrastructure\Repository\Loader\SqlFieldMetadataLoader;
24use App\Core\Engine\Infrastructure\Repository\Loader\SqlFilterMetadataLoader;
25use 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 */
35final 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}