Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
83.82% covered (warning)
83.82%
57 / 68
87.50% covered (warning)
87.50%
7 / 8
CRAP
0.00% covered (danger)
0.00%
0 / 1
SqlFieldMetadataLoader
85.07% covered (warning)
85.07%
57 / 67
87.50% covered (warning)
87.50%
7 / 8
25.92
0.00% covered (danger)
0.00%
0 / 1
 __construct
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 findFields
100.00% covered (success)
100.00%
10 / 10
100.00% covered (success)
100.00%
1 / 1
2
 findGlobalSearchFields
0.00% covered (danger)
0.00%
0 / 10
0.00% covered (danger)
0.00%
0 / 1
6
 findSections
100.00% covered (success)
100.00%
9 / 9
100.00% covered (success)
100.00%
1 / 1
2
 enrichPicklistBatch
100.00% covered (success)
100.00%
5 / 5
100.00% covered (success)
100.00%
1 / 1
2
 collectPicklistIds
100.00% covered (success)
100.00%
8 / 8
100.00% covered (success)
100.00%
1 / 1
5
 fetchPicklistValues
100.00% covered (success)
100.00%
17 / 17
100.00% covered (success)
100.00%
1 / 1
4
 applyPicklistValues
100.00% covered (success)
100.00%
7 / 7
100.00% covered (success)
100.00%
1 / 1
6
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\Loader;
8
9defined('AMMONLY_APP') || exit('Direct script access is forbidden.');
10
11use App\Core\Engine\Domain\Model\FieldMetadata;
12use App\Core\Engine\Domain\Model\SectionMetadata;
13use PDO;
14
15/**
16 * SQL Field and Section Metadata Loader.
17 *
18 * @package App\Core\Engine\Infrastructure\Repository\Loader
19 */
20final readonly class SqlFieldMetadataLoader
21{
22    /**
23     * SqlFieldMetadataLoader constructor.
24     *
25     * @param PDO $pdo Database connection.
26     */
27    public function __construct(
28        private PDO $pdo
29    ) {
30    }
31
32    /**
33     * Returns all field definitions for a given module, ordered by sort_order.
34     *
35     * @param int $moduleId Module primary key.
36     * @return array<int, FieldMetadata> Ordered list of field metadata value objects.
37     */
38    public function findFields(int $moduleId): array
39    {
40        $sql = 'SELECT `f`.`id`, `f`.`module_id`, `f`.`field_key`, `f`.`column_expression`,
41                       `f`.`label`, `f`.`is_sortable`, `f`.`is_filterable`,
42                       `f`.`is_mandatory`, `f`.`is_unique`, `f`.`is_system`, `f`.`is_readonly`,
43                       `f`.`is_summary`, `f`.`is_quick_create`, `f`.`is_global_search`,
44                       `f`.`sort_order`, `f`.`section_id`, `f`.`picklist_id`,
45                       `f`.`relation_module`, `f`.`relation_table`,
46                       `f`.`relation_key`, `f`.`relation_label`, `f`.`relation_display`,
47                       `f`.`validation_rules`, `f`.`filter_options`,
48                       `f`.`default_value`, `f`.`placeholder`, `f`.`hidden_views`,
49                       `u`.`name` AS `uitype_name`
50                FROM   `a_core_field_records` `f`
51                JOIN   `a_core_uitype_records` `u` ON `u`.`id` = `f`.`uitype_id`
52                WHERE  `f`.`module_id` = :module_id
53                ORDER  BY `f`.`sort_order` ASC, `f`.`id` ASC';
54
55        $stmt = $this->pdo->prepare($sql);
56        $stmt->execute([':module_id' => $moduleId]);
57        /** @var array<int, array<string, mixed>> $rows */
58        $rows = $stmt->fetchAll(PDO::FETCH_ASSOC);
59
60        $this->enrichPicklistBatch($rows);
61
62        $fields = [];
63        foreach ($rows as $row) {
64            $fields[] = FieldMetadata::fromRow($row);
65        }
66
67        return $fields;
68    }
69
70    /**
71     * Returns field definitions configured for global search for a given module.
72     *
73     * @param int $moduleId Module primary key.
74     * @return array<int, FieldMetadata> Ordered list of global search fields.
75     */
76    public function findGlobalSearchFields(int $moduleId): array
77    {
78        $sql = 'SELECT `f`.`id`, `f`.`module_id`, `f`.`field_key`, `f`.`column_expression`,
79                       `f`.`label`, `f`.`is_sortable`, `f`.`is_filterable`,
80                       `f`.`is_mandatory`, `f`.`is_unique`, `f`.`is_system`, `f`.`is_readonly`,
81                       `f`.`is_summary`, `f`.`is_quick_create`, `f`.`is_global_search`,
82                       `f`.`sort_order`, `f`.`section_id`, `f`.`picklist_id`,
83                       `f`.`relation_module`, `f`.`relation_table`,
84                       `f`.`relation_key`, `f`.`relation_label`, `f`.`relation_display`,
85                       `f`.`validation_rules`, `f`.`filter_options`,
86                       `f`.`default_value`, `f`.`placeholder`, `f`.`hidden_views`,
87                       `u`.`name` AS `uitype_name`
88                FROM   `a_core_field_records` `f`
89                JOIN   `a_core_uitype_records` `u` ON `u`.`id` = `f`.`uitype_id`
90                WHERE  `f`.`module_id` = :module_id
91                  AND  `f`.`is_global_search` = 1
92                  AND  `f`.`is_system` = 0
93                ORDER  BY `f`.`sort_order` ASC, `f`.`id` ASC';
94
95        $stmt = $this->pdo->prepare($sql);
96        $stmt->execute([':module_id' => $moduleId]);
97        /** @var array<int, array<string, mixed>> $rows */
98        $rows = $stmt->fetchAll(PDO::FETCH_ASSOC);
99
100        $this->enrichPicklistBatch($rows);
101
102        $fields = [];
103        foreach ($rows as $row) {
104            $fields[] = FieldMetadata::fromRow($row);
105        }
106
107        return $fields;
108    }
109
110    /**
111     * Returns all logical field sections configured for a given module, ordered by sort_order.
112     *
113     * @param int $moduleId Module primary key.
114     * @return array<int, SectionMetadata> Ordered list of section metadata value objects.
115     */
116    public function findSections(int $moduleId): array
117    {
118        $sql = 'SELECT `id`, `module_id`, `name`, `label`, `icon_class`,
119                       `sort_order`, `is_active`, `is_system`, `is_default`
120                FROM   `a_core_section_records`
121                WHERE  `module_id` = :module_id
122                  AND  `is_active` = 1
123                ORDER  BY `sort_order` ASC, `id` ASC';
124
125        $stmt = $this->pdo->prepare($sql);
126        $stmt->execute([':module_id' => $moduleId]);
127        /** @var array<int, array<string, mixed>> $rows */
128        $rows = $stmt->fetchAll(PDO::FETCH_ASSOC);
129        $sections = [];
130
131        foreach ($rows as $row) {
132            $sections[] = SectionMetadata::fromRow($row);
133        }
134
135        return $sections;
136    }
137
138    /**
139     * Enriches field filter options from picklists in a single batch query.
140     *
141     * @param array<int, array<string, mixed>> $rows Field rows.
142     */
143    private function enrichPicklistBatch(array &$rows): void
144    {
145        $pids = $this->collectPicklistIds($rows);
146        if ($pids === []) {
147            return;
148        }
149
150        [$grouped, $defaultVals] = $this->fetchPicklistValues(array_keys($pids));
151        $this->applyPicklistValues($rows, $pids, $grouped, $defaultVals);
152    }
153
154    /**
155     * @param array<int, array<string, mixed>> $rows
156     * @return array<int, list<int>>
157     */
158    private function collectPicklistIds(array $rows): array
159    {
160        $pids = [];
161        foreach ($rows as $i => $row) {
162            $pid = (int) ($row['picklist_id'] ?? 0);
163            $opts = $row['filter_options'] ?? null;
164            $hasOptions = !empty($opts) && $opts !== '[]';
165            if ($pid > 0 && !$hasOptions) {
166                $pids[$pid][] = $i;
167            }
168        }
169
170        return $pids;
171    }
172
173    /**
174     * @param list<int> $pidKeys
175     * @return array{0: array<int, array<string, string>>, 1: array<int, string>}
176     */
177    private function fetchPicklistValues(array $pidKeys): array
178    {
179        $placeholders = implode(',', array_fill(0, count($pidKeys), '?'));
180        $valSql = sprintf(
181            'SELECT `picklist_id`, `value`, `label`, `is_default` FROM `a_core_picklist_value_records` ' .
182            'WHERE `picklist_id` IN (%s) AND `is_active` = 1 ORDER BY `sort_order` ASC, `label` ASC',
183            $placeholders
184        );
185
186        $stmt = $this->pdo->prepare($valSql);
187        $stmt->execute($pidKeys);
188        /**
189         * @var array<int, array{
190         *     picklist_id: int|string,
191         *     value: string,
192         *     label: string,
193         *     is_default?: int|string
194         * }> $valRows
195         */
196        $valRows = $stmt->fetchAll(PDO::FETCH_ASSOC);
197
198        $grouped = [];
199        $defaultVals = [];
200        foreach ($valRows as $v) {
201            $pId = (int) $v['picklist_id'];
202            $grouped[$pId][$v['value']] = $v['label'];
203            if (!empty($v['is_default']) && !isset($defaultVals[$pId])) {
204                $defaultVals[$pId] = (string) $v['value'];
205            }
206        }
207
208        return [$grouped, $defaultVals];
209    }
210
211    /**
212     * @param array<int, array<string, mixed>>     $rows
213     * @param array<int, list<int>>                 $pids
214     * @param array<int, array<string, string>>     $grouped
215     * @param array<int, string>                    $defaultVals
216     */
217    private function applyPicklistValues(
218        array &$rows,
219        array $pids,
220        array $grouped,
221        array $defaultVals
222    ): void {
223        foreach ($pids as $pId => $rowIndices) {
224            if (!isset($grouped[$pId])) {
225                continue;
226            }
227            $encoded = (string) json_encode($grouped[$pId], JSON_UNESCAPED_UNICODE);
228            foreach ($rowIndices as $idx) {
229                $rows[$idx]['filter_options'] = $encoded;
230                if (empty($rows[$idx]['default_value']) && isset($defaultVals[$pId])) {
231                    $rows[$idx]['default_value'] = $defaultVals[$pId];
232                }
233            }
234        }
235    }
236}