PluginProbe
The Innovative Form Builder – IvyForms / 1.5.1
The Innovative Form Builder – IvyForms v1.5.1
1.5.1 1.5 1.4.1 1.4 trunk 0.1.2 0.2 0.2.1 0.3 0.3.1 0.4 0.5 0.6 0.6.1 0.6.1-backup 0.6.1.1 0.7 0.8 0.8.1 0.8.2 0.9 0.9.1 1.0 1.1 1.1.1 All 27 releases
ivyforms / backend / src / Common / Helpers / Import / FluentFormsFieldMapper.php

FluentFormsFieldMapper.php in The Innovative Form Builder – IvyForms 1.5.1, at backend/src/Common/Helpers/Import/FluentFormsFieldMapper.php

342 lines 10.9 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2
3 namespace IvyForms\Common\Helpers\Import;
4
5 /**
6 * Map individual Fluent Forms elements into IvyForms field arrays.
7 *
8 * Splitting field mapping out of FluentFormsImportConverter keeps each class
9 * focused and within complexity limits.
10 */
11 class FluentFormsFieldMapper
12 {
13 /**
14 * Map Fluent Forms elements to IvyForms field types. Elements absent from
15 * this map are skipped during import.
16 *
17 * @var array<string, string>
18 */
19 private const ELEMENT_MAP = [
20 'input_text' => 'text',
21 'input_mask' => 'text',
22 'input_email' => 'email',
23 'input_number' => 'number',
24 'textarea' => 'textarea',
25 'select' => 'select',
26 'select_country' => 'select',
27 'input_radio' => 'radio',
28 'input_checkbox' => 'checkbox',
29 'input_url' => 'website',
30 'input_password' => 'password',
31 'phone' => 'phone',
32 'input_phone' => 'phone',
33 'input_date' => 'date',
34 'ratings' => 'rating',
35 'gdpr_agreement' => 'gdpr',
36 'input_file' => 'file-upload',
37 'input_image' => 'file-upload',
38 'custom_html' => 'html',
39 ];
40
41 /**
42 * Field types that carry selectable options.
43 *
44 * @var array<int, string>
45 */
46 private const OPTION_TYPES = ['select', 'radio', 'checkbox', 'multi-select'];
47
48 /**
49 * Sub-field key to IvyForms name field type mapping.
50 *
51 * @var array<string, string>
52 */
53 private const NAME_FIELD_MAP = [
54 'first_name' => 'nameField1',
55 'last_name' => 'nameField2',
56 'middle_name' => 'nameField3',
57 ];
58
59 /**
60 * Map a single Fluent Forms element and append the resulting field(s).
61 *
62 * @param array<string, mixed> $rawField
63 * @param array<int, array<string, mixed>> $fields
64 * @param array<string, int> $context
65 * @return void
66 */
67 public static function append(array $rawField, array &$fields, array &$context): void
68 {
69 $element = (string) ($rawField['element'] ?? '');
70
71 if ($element === 'container') {
72 self::appendContainerFields($rawField, $fields, $context);
73 return;
74 }
75
76 if ($element === 'input_name') {
77 self::appendNameField($rawField, $fields, $context);
78 return;
79 }
80
81 $ivyType = self::resolveType($element, $rawField);
82 if ($ivyType === null) {
83 return;
84 }
85
86 if ($ivyType === 'html' && self::extractHtmlContent($rawField) === '') {
87 // Skip empty HTML blocks — nothing useful to import.
88 return;
89 }
90
91 $fields[] = self::buildSimpleField($ivyType, $rawField, $context);
92 }
93
94 /**
95 * Build a single IvyForms field from a mapped element.
96 *
97 * @param string $ivyType
98 * @param array<string, mixed> $rawField
99 * @param array<string, int> $context
100 * @return array<string, mixed>
101 */
102 private static function buildSimpleField(string $ivyType, array $rawField, array &$context): array
103 {
104 $attributes = self::child($rawField, 'attributes');
105 $settings = self::child($rawField, 'settings');
106 $validation = self::child($settings, 'validation_rules');
107 $required = self::child($validation, 'required');
108
109 $field = [
110 'id' => $context['id']++,
111 'fieldIndex' => $context['fieldIndex']++,
112 'type' => $ivyType,
113 'label' => self::resolveLabel($settings),
114 'placeholder' => self::text($attributes, 'placeholder'),
115 'description' => self::text($settings, 'help_message'),
116 'required' => !empty($required['value']),
117 'requiredMessage' => self::text($required, 'message'),
118 'defaultValue' => self::text($attributes, 'value'),
119 'parentId' => null,
120 'position' => $context['position']++,
121 'rowIndex' => $context['rowIndex']++,
122 'columnIndex' => 0,
123 'width' => 100,
124 ];
125
126 if ($ivyType === 'html') {
127 $field['htmlContent'] = self::extractHtmlContent($rawField);
128 }
129
130 if ($ivyType === 'password') {
131 // Match Pro password field defaults; Fluent Forms has no confirm field.
132 $field['confirmFieldEnabled'] = false;
133 $field['showPasswordIcon'] = true;
134 $field['inputPrefix'] = self::text($settings, 'prefix_label');
135 $field['inputSuffix'] = self::text($settings, 'suffix_label');
136 }
137
138 if (in_array($ivyType, self::OPTION_TYPES, true)) {
139 $options = FluentFormsOptionsExtractor::extract($settings, $attributes);
140 if (!empty($options)) {
141 $field['fieldOptions'] = $options;
142 }
143 }
144
145 return $field;
146 }
147
148 /**
149 * Extract sanitized HTML from a Fluent Forms custom_html element.
150 *
151 * @param array<string, mixed> $rawField
152 * @return string
153 */
154 private static function extractHtmlContent(array $rawField): string
155 {
156 $settings = self::child($rawField, 'settings');
157
158 return wp_kses_post((string) ($settings['html_codes'] ?? ''));
159 }
160
161 /**
162 * Flatten a container's columns into top-level fields.
163 *
164 * @param array<string, mixed> $rawField
165 * @param array<int, array<string, mixed>> $fields
166 * @param array<string, int> $context
167 * @return void
168 */
169 private static function appendContainerFields(array $rawField, array &$fields, array &$context): void
170 {
171 foreach (($rawField['columns'] ?? []) as $column) {
172 if (!is_array($column)) {
173 continue;
174 }
175 foreach (($column['fields'] ?? []) as $columnField) {
176 if (is_array($columnField)) {
177 self::append($columnField, $fields, $context);
178 }
179 }
180 }
181 }
182
183 /**
184 * Build an IvyForms composite name field (parent + visible sub-fields).
185 *
186 * @param array<string, mixed> $rawField
187 * @param array<int, array<string, mixed>> $fields
188 * @param array<string, int> $context
189 * @return void
190 */
191 private static function appendNameField(array $rawField, array &$fields, array &$context): void
192 {
193 $settings = self::child($rawField, 'settings');
194 $required = !empty(self::child(self::child($settings, 'validation_rules'), 'required')['value']);
195
196 $parentIndex = $context['fieldIndex']++;
197 $fields[] = [
198 'id' => $context['id']++,
199 'fieldIndex' => $parentIndex,
200 'type' => 'name',
201 'label' => self::resolveLabel($settings),
202 'placeholder' => '',
203 'required' => $required,
204 'defaultValue' => '',
205 'parentId' => null,
206 'position' => $context['position']++,
207 'rowIndex' => $context['rowIndex']++,
208 'columnIndex' => 0,
209 'width' => 100,
210 ];
211
212 self::appendNameSubFields($rawField, $fields, $context, $parentIndex, $required);
213 }
214
215 /**
216 * Append the visible sub-fields (first/last/middle) of a name field.
217 *
218 * @param array<string, mixed> $rawField
219 * @param array<int, array<string, mixed>> $fields
220 * @param array<string, int> $context
221 * @param int $parentIndex
222 * @param bool $required
223 * @return void
224 */
225 private static function appendNameSubFields(
226 array $rawField,
227 array &$fields,
228 array &$context,
229 int $parentIndex,
230 bool $required
231 ): void {
232 $subPosition = 1;
233 foreach (self::child($rawField, 'fields') as $key => $subField) {
234 if (!is_array($subField) || self::isHiddenSubField($subField)) {
235 continue;
236 }
237
238 $subSettings = self::child($subField, 'settings');
239 $subAttributes = self::child($subField, 'attributes');
240 $nameFieldType = self::NAME_FIELD_MAP[(string) $key] ?? ('nameField' . $subPosition);
241
242 $fields[] = [
243 'id' => $context['id']++,
244 'fieldIndex' => $parentIndex,
245 'type' => 'text',
246 'label' => self::text($subSettings, 'label'),
247 'placeholder' => self::text($subAttributes, 'placeholder'),
248 'required' => $required,
249 'defaultValue' => '',
250 'parentId' => $parentIndex,
251 'position' => $subPosition,
252 'settings' => wp_json_encode(['nameFieldType' => $nameFieldType]),
253 ];
254 $subPosition++;
255 }
256 }
257
258 /**
259 * Whether a name sub-field is explicitly hidden.
260 *
261 * @param array<string, mixed> $subField
262 * @return bool
263 */
264 private static function isHiddenSubField(array $subField): bool
265 {
266 $settings = self::child($subField, 'settings');
267
268 return ($settings['visible'] ?? true) === false;
269 }
270
271 /**
272 * Resolve the IvyForms field type for a Fluent Forms element.
273 *
274 * @param string $element
275 * @param array<string, mixed> $rawField
276 * @return string|null
277 */
278 private static function resolveType(string $element, array $rawField): ?string
279 {
280 $ivyType = self::ELEMENT_MAP[$element] ?? null;
281 if ($ivyType === null) {
282 return null;
283 }
284
285 if ($ivyType === 'select' && !empty(self::child($rawField, 'attributes')['multiple'])) {
286 return 'multi-select';
287 }
288
289 return $ivyType;
290 }
291
292 /**
293 * Resolve a field label, falling back to the admin label.
294 *
295 * @param array<string, mixed> $settings
296 * @return string
297 */
298 private static function resolveLabel(array $settings): string
299 {
300 $label = self::text($settings, 'label');
301 if ($label === '') {
302 $label = self::text($settings, 'admin_field_label');
303 }
304
305 return $label;
306 }
307
308 /**
309 * Get a nested array value by key, defaulting to an empty array.
310 *
311 * @param array<string, mixed> $source
312 * @param string $key
313 * @return array<string, mixed>
314 */
315 private static function child(array $source, string $key): array
316 {
317 $value = $source[$key] ?? null;
318
319 return is_array($value) ? $value : [];
320 }
321
322 /**
323 * Get a sanitized string value from an array by key.
324 *
325 * Fluent Forms stores multi-value data (such as checkbox defaults) as arrays,
326 * so non-scalars are discarded instead of being cast to string.
327 *
328 * @param array<string, mixed> $source
329 * @param string $key
330 * @return string
331 */
332 private static function text(array $source, string $key): string
333 {
334 $value = $source[$key] ?? '';
335 if (!is_scalar($value)) {
336 return '';
337 }
338
339 return sanitize_text_field((string) $value);
340 }
341 }
342