Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
99.22% covered (success)
99.22%
127 / 128
88.89% covered (warning)
88.89%
8 / 9
CRAP
0.00% covered (danger)
0.00%
0 / 1
ModuleFieldDto
99.21% covered (success)
99.21%
126 / 127
88.89% covered (warning)
88.89%
8 / 9
22
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
 fromArray
100.00% covered (success)
100.00%
44 / 44
100.00% covered (success)
100.00%
1 / 1
4
 resolveFieldKey
100.00% covered (success)
100.00%
7 / 7
100.00% covered (success)
100.00%
1 / 1
3
 resolveColumnExpression
75.00% covered (warning)
75.00%
3 / 4
0.00% covered (danger)
0.00%
0 / 1
4.25
 resolveBoolFlag
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
1 / 1
3
 resolveStringParam
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
2
 resolveIntParam
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
2
 resolveSortOrder
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
2
 toArray
100.00% covered (success)
100.00%
61 / 61
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\ModuleBuilder\Domain\Model;
8
9defined('AMMONLY_APP') || exit('Direct script access is forbidden.');
10
11/**
12 * Module Field Definition DTO.
13 *
14 * @package App\Core\ModuleBuilder\Domain\Model
15 */
16final readonly class ModuleFieldDto
17{
18    /**
19     * @param string                $fieldKey         Machine key column name (e.g. 'total_amount').
20     * @param string                $label            Display label (e.g. 'Total Amount').
21     * @param string                $uitypeName       UiType identifier (e.g. 'currency_amount').
22     * @param int|null              $id               Primary key if existing field.
23     * @param int|null              $uitypeId         UiType numeric ID.
24     * @param string|null           $sectionName      Target section machine name.
25     * @param int|null              $sectionId        Target section ID if existing.
26     * @param string                $columnExpression SQL column expression (e.g. 'sc.total_amount').
27     * @param string                $sqlType          Physical SQL column type (e.g. 'DECIMAL(15,2)').
28     * @param bool                  $isMandatory      Whether field is required on write.
29     * @param bool                  $isUnique         Whether field requires UNIQUE constraint.
30     * @param bool                  $isSortable       Whether field can be sorted in DataGrid.
31     * @param bool                  $isFilterable     Whether field can be filtered in DataGrid.
32     * @param bool                  $isSummary        Whether field is displayed on Kanban cards.
33     * @param bool                  $isQuickCreate    Whether field appears in quick create modal.
34     * @param bool                  $isGlobalSearch   Whether field is searchable in global search.
35     * @param bool                  $isSystem         Whether field is protected system field.
36     * @param bool                  $isReadonly       Whether field is read-only.
37     * @param string|null           $defaultValue     Default value.
38     * @param string|null           $placeholder      Input placeholder hint.
39     * @param string|null           $hiddenViews      Comma-separated views where field is hidden.
40     * @param int|null              $picklistId       FK to a_core_picklist_records.
41     * @param string|null           $relationModule   Target module machine name if relation.
42     * @param string|null           $relationTable    Target SQL table if relation.
43     * @param string|null           $relationKey      Target primary key column if relation.
44     * @param string|null           $relationLabel    Target label column if relation.
45     * @param list<string>          $options          Predefined options for single_select.
46     * @param array<string, mixed>  $validationRules  JSON validation rules.
47     * @param int                   $sortOrder        Visual order position.
48     */
49    public function __construct(
50        public string $fieldKey,
51        public string $label,
52        public string $uitypeName,
53        public ?int $id = null,
54        public ?int $uitypeId = null,
55        public ?string $sectionName = null,
56        public ?int $sectionId = null,
57        public string $columnExpression = '',
58        public string $sqlType = 'VARCHAR(255)',
59        public bool $isMandatory = false,
60        public bool $isUnique = false,
61        public bool $isSortable = true,
62        public bool $isFilterable = true,
63        public bool $isSummary = false,
64        public bool $isQuickCreate = false,
65        public bool $isGlobalSearch = false,
66        public bool $isSystem = false,
67        public bool $isReadonly = false,
68        public ?string $defaultValue = null,
69        public ?string $placeholder = null,
70        public ?string $hiddenViews = null,
71        public ?int $picklistId = null,
72        public ?string $relationModule = null,
73        public ?string $relationTable = null,
74        public ?string $relationKey = null,
75        public ?string $relationLabel = null,
76        public array $options = [],
77        public array $validationRules = [],
78        public int $sortOrder = 10
79    ) {
80    }
81
82    /**
83     * Creates instance from array with smart defaults.
84     *
85     * @param array<string, mixed> $data
86     * @param string               $tableAlias
87     * @return self
88     */
89    public static function fromArray(array $data, string $tableAlias = 't'): self
90    {
91        $label = trim((string) ($data['label'] ?? ''));
92        $key = self::resolveFieldKey($data, $label);
93        $expr = self::resolveColumnExpression($data, $key, $tableAlias);
94
95        $rawOpts = (array) ($data['options'] ?? []);
96        $cleanOpts = array_values(array_filter(array_map('trim', array_map('strval', $rawOpts))));
97
98        $secName = self::resolveStringParam($data, 'section_name', 'sectionName');
99        $secId = self::resolveIntParam($data, 'section_id', 'sectionId');
100        $sort = self::resolveSortOrder($data);
101        $uitypeId = self::resolveIntParam($data, 'uitype_id', 'uitypeId');
102        $picklistId = self::resolveIntParam($data, 'picklist_id', 'picklistId');
103        $id = isset($data['id']) && is_numeric($data['id']) ? (int) $data['id'] : null;
104
105        $rawSql = $data['sql_type'] ?? $data['sqlType'] ?? $data['column_type'] ??
106            $data['columnType'] ?? 'VARCHAR(255)';
107
108        return new self(
109            fieldKey: $key,
110            label: $label,
111            uitypeName: (string) ($data['uitype_name'] ?? $data['uitypeName'] ?? $data['uitype'] ?? 'string_input'),
112            id: $id,
113            uitypeId: $uitypeId,
114            sectionName: $secName,
115            sectionId: $secId,
116            columnExpression: $expr,
117            sqlType: (string) $rawSql,
118            isMandatory: self::resolveBoolFlag($data, ['is_mandatory', 'isMandatory', 'is_required']),
119            isUnique: self::resolveBoolFlag($data, ['is_unique', 'isUnique']),
120            isSortable: self::resolveBoolFlag($data, ['is_sortable', 'isSortable'], true),
121            isFilterable: self::resolveBoolFlag($data, ['is_filterable', 'isFilterable'], true),
122            isSummary: self::resolveBoolFlag($data, ['is_summary', 'isSummary']),
123            isQuickCreate: self::resolveBoolFlag($data, ['is_quick_create', 'isQuickCreate']),
124            isGlobalSearch: self::resolveBoolFlag($data, ['is_global_search', 'isGlobalSearch', 'isSearchable']),
125            isSystem: self::resolveBoolFlag($data, ['is_system', 'isSystem']),
126            isReadonly: self::resolveBoolFlag($data, ['is_readonly', 'isReadonly']),
127            defaultValue: self::resolveStringParam($data, 'default_value', 'defaultValue'),
128            placeholder: isset($data['placeholder']) ? (string) $data['placeholder'] : null,
129            hiddenViews: self::resolveStringParam($data, 'hidden_views', 'hiddenViews'),
130            picklistId: $picklistId,
131            relationModule: self::resolveStringParam($data, 'relation_module', 'relationModule'),
132            relationTable: self::resolveStringParam($data, 'relation_table', 'relationTable'),
133            relationKey: self::resolveStringParam($data, 'relation_key', 'relationKey'),
134            relationLabel: self::resolveStringParam($data, 'relation_label', 'relationLabel'),
135            options: $cleanOpts,
136            validationRules: (array) ($data['validation_rules'] ?? $data['validationRules'] ?? []),
137            sortOrder: $sort ?? 10
138        );
139    }
140
141    private static function resolveFieldKey(array $data, string $label): string
142    {
143        $rawKey = $data['field_key'] ?? $data['fieldKey'] ?? $data['column_name'] ??
144            $data['columnName'] ?? $data['key'] ?? '';
145        $key = trim((string) $rawKey);
146        if ($key === '' && $label !== '') {
147            $key = strtolower((string) preg_replace('/\W/', '_', $label));
148            $key = trim((string) preg_replace('/_+/', '_', $key), '_');
149        }
150        return $key;
151    }
152
153    private static function resolveColumnExpression(array $data, string $key, string $tableAlias): string
154    {
155        $expr = trim((string) ($data['column_expression'] ?? $data['columnExpression'] ?? ''));
156        if ($expr === '' && $key !== '') {
157            return ($tableAlias !== '' ? $tableAlias . '.' : '') . $key;
158        }
159        return $expr;
160    }
161
162    /**
163     * Resolves boolean flag from multiple candidate keys with a fallback default.
164     *
165     * @param array<string, mixed> $data
166     * @param string[]             $keys
167     * @param bool                 $default
168     * @return bool
169     */
170    private static function resolveBoolFlag(array $data, array $keys, bool $default = false): bool
171    {
172        foreach ($keys as $k) {
173            if (array_key_exists($k, $data)) {
174                return (bool) $data[$k];
175            }
176        }
177        return $default;
178    }
179
180    private static function resolveStringParam(array $data, string $snakeKey, string $camelKey): ?string
181    {
182        $val = $data[$snakeKey] ?? $data[$camelKey] ?? null;
183        return $val !== null ? (string) $val : null;
184    }
185
186    private static function resolveIntParam(array $data, string $snakeKey, string $camelKey): ?int
187    {
188        $val = $data[$snakeKey] ?? $data[$camelKey] ?? null;
189        return is_numeric($val) ? (int) $val : null;
190    }
191
192    private static function resolveSortOrder(array $data): ?int
193    {
194        $val = $data['sort_order'] ?? $data['sortOrder'] ?? $data['displayOrder'] ?? null;
195        return is_numeric($val) ? (int) $val : null;
196    }
197
198    /**
199     * Serializes to array.
200     *
201     * @return array<string, mixed>
202     */
203    public function toArray(): array
204    {
205        return [
206            'id' => $this->id,
207            'field_key' => $this->fieldKey,
208            'fieldKey' => $this->fieldKey,
209            'column_name' => $this->fieldKey,
210            'columnName' => $this->fieldKey,
211            'label' => $this->label,
212            'uitype_name' => $this->uitypeName,
213            'uitypeName' => $this->uitypeName,
214            'uitype_id' => $this->uitypeId,
215            'uitypeId' => $this->uitypeId,
216            'section_name' => $this->sectionName,
217            'sectionName' => $this->sectionName,
218            'section_id' => $this->sectionId,
219            'sectionId' => $this->sectionId,
220            'column_expression' => $this->columnExpression,
221            'columnExpression' => $this->columnExpression,
222            'sql_type' => $this->sqlType,
223            'sqlType' => $this->sqlType,
224            'column_type' => $this->sqlType,
225            'columnType' => $this->sqlType,
226            'is_mandatory' => $this->isMandatory,
227            'isMandatory' => $this->isMandatory,
228            'is_unique' => $this->isUnique,
229            'isUnique' => $this->isUnique,
230            'is_sortable' => $this->isSortable,
231            'isSortable' => $this->isSortable,
232            'is_filterable' => $this->isFilterable,
233            'isFilterable' => $this->isFilterable,
234            'is_summary' => $this->isSummary,
235            'isSummary' => $this->isSummary,
236            'is_quick_create' => $this->isQuickCreate,
237            'isQuickCreate' => $this->isQuickCreate,
238            'is_global_search' => $this->isGlobalSearch,
239            'isGlobalSearch' => $this->isGlobalSearch,
240            'isSearchable' => $this->isGlobalSearch,
241            'is_system' => $this->isSystem,
242            'isSystem' => $this->isSystem,
243            'is_readonly' => $this->isReadonly,
244            'isReadonly' => $this->isReadonly,
245            'default_value' => $this->defaultValue,
246            'defaultValue' => $this->defaultValue,
247            'placeholder' => $this->placeholder,
248            'hidden_views' => $this->hiddenViews,
249            'hiddenViews' => $this->hiddenViews,
250            'picklist_id' => $this->picklistId,
251            'picklistId' => $this->picklistId,
252            'relation_module' => $this->relationModule,
253            'relationModule' => $this->relationModule,
254            'relation_table' => $this->relationTable,
255            'relationTable' => $this->relationTable,
256            'relation_key' => $this->relationKey,
257            'relationKey' => $this->relationKey,
258            'relation_label' => $this->relationLabel,
259            'relationLabel' => $this->relationLabel,
260            'options' => $this->options,
261            'validation_rules' => $this->validationRules,
262            'validationRules' => $this->validationRules,
263            'sort_order' => $this->sortOrder,
264            'sortOrder' => $this->sortOrder,
265        ];
266    }
267}