PluginProbe
The Innovative Form Builder – IvyForms / 1.4
The Innovative Form Builder – IvyForms v1.4
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 / McpHelpers / McpFieldPayloadHelper.php

McpFieldPayloadHelper.php in The Innovative Form Builder – IvyForms 1.4, at backend/src/Common/Helpers/McpHelpers/McpFieldPayloadHelper.php

595 lines 21.2 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2
3 /**
4 * @copyright © Melograno Venture Studio. All rights reserved.
5 * @licence See COPYING.md for license details.
6 */
7
8 namespace IvyForms\Common\Helpers\McpHelpers;
9
10 use IvyForms\Common\Sanitizer\HtmlSanitizer;
11 use IvyForms\Services\Translations\BackendStrings;
12
13 /**
14 * Extracted payload logic for IvyForms MCP field operations.
15 *
16 * Keeping the heavy branching outside `McpFieldAbilitiesHelper` reduces
17 * cyclomatic/overall complexity for that class.
18 */
19 class McpFieldPayloadHelper
20 {
21 /**
22 * @param array<string, mixed> $input
23 * @return array{label: string, fields: array<int, array<string, mixed>>}
24 */
25 public static function buildFieldPayload(
26 array $input,
27 int $formId,
28 int $position,
29 int $rowIndex,
30 int $fieldIndex
31 ): array {
32 $type = sanitize_text_field($input['type'] ?? 'text');
33 $label = sanitize_text_field($input['label'] ?? self::defaultLabelForType($type));
34 $widthInput = (int) ($input['width'] ?? 100);
35 $width = in_array($widthInput, [25, 50, 75, 100], true) ? $widthInput : 100;
36 $required = array_key_exists('required', $input)
37 ? !empty($input['required'])
38 : self::defaultRequiredForType($type);
39
40 $newField = array_merge(
41 self::baseField($type, $formId, $fieldIndex, $position, $rowIndex, $width, $label, $required, $input),
42 McpFieldTypeDefaultsHelper::defaultsForType($type, $input)
43 );
44
45 if (!empty($input['pageId']) && is_string($input['pageId'])) {
46 McpMultiPageHelper::applyPageIdToField($newField, sanitize_text_field($input['pageId']));
47 }
48
49 $newFields = self::buildRelatedFields($type, $input, $newField, $formId, $fieldIndex, $position);
50 self::propagatePageIdToCompoundFields($newFields, $newField, $fieldIndex);
51
52 return [
53 'label' => $label,
54 'fields' => $newFields,
55 ];
56 }
57
58 /**
59 * @param array<string, mixed> $input
60 * @param array<string, mixed> $newField
61 * @return array<int, array<string, mixed>>
62 */
63 private static function buildRelatedFields(
64 string $type,
65 array $input,
66 array $newField,
67 int $formId,
68 int $fieldIndex,
69 int $position
70 ): array {
71 $newFields = [$newField];
72
73 if (in_array($type, ['name', 'address'], true)) {
74 return array_merge(
75 $newFields,
76 self::compoundChildFields($type, $formId, $fieldIndex, $position)
77 );
78 }
79
80 if ($type !== 'likert') {
81 return $newFields;
82 }
83
84 $likertRows = self::resolveLikertRows($input);
85
86 return array_merge(
87 $newFields,
88 self::likertRowChildFields($likertRows, $formId, $fieldIndex, $position)
89 );
90 }
91
92 /**
93 * @param array<int, array<string, mixed>> $newFields
94 * @param array<string, mixed> $newField
95 */
96 private static function propagatePageIdToCompoundFields(
97 array &$newFields,
98 array $newField,
99 int $fieldIndex
100 ): void {
101 if (empty($newField['pageId']) || count($newFields) <= 1) {
102 return;
103 }
104
105 foreach ($newFields as &$compoundField) {
106 if ($compoundField === $newField) {
107 continue;
108 }
109 if ((int) ($compoundField['fieldIndex'] ?? 0) === $fieldIndex) {
110 McpMultiPageHelper::applyPageIdToField($compoundField, (string) $newField['pageId']);
111 }
112 }
113 unset($compoundField);
114 }
115
116 /**
117 * @param array<string, mixed> $input
118 * @return array<string, mixed>
119 */
120 private static function baseField(
121 string $type,
122 int $formId,
123 int $fieldIndex,
124 int $position,
125 int $rowIndex,
126 int $width,
127 string $label,
128 bool $required,
129 array $input
130 ): array {
131 return [
132 'id' => 0,
133 'formId' => $formId,
134 'fieldIndex' => $fieldIndex,
135 'type' => $type,
136 'position' => $position,
137 'rowIndex' => $rowIndex,
138 'columnIndex' => 0,
139 'width' => $width,
140 'parentId' => null,
141 'rows' => 0,
142 'label' => $label,
143 'required' => $required,
144 'placeholder' => sanitize_text_field($input['placeholder'] ?? ''),
145 'hideLabel' => false,
146 'readOnly' => false,
147 'description' => HtmlSanitizer::sanitizeFieldDescription($input['description'] ?? ''),
148 'requiredMessage' => '',
149 'cssClasses' => '',
150 'shuffleOptions' => false,
151 'enableSearch' => false,
152 'phoneFormat' => 'international',
153 'phoneAutoDetect' => $type === 'phone',
154 'minValue' => null,
155 'maxValue' => null,
156 'step' => 1,
157 'numberFormat' => '',
158 'visible' => true,
159 'showRatingText' => false,
160 'htmlContent' => '',
161 'limitRange' => false,
162 'confirmFieldEnabled' => false,
163 'confirmFieldLabel' => '',
164 'confirmFieldPlaceholder' => '',
165 'confirmFieldHideLabel' => false,
166 'timeFieldType' => '',
167 'timeFormat' => '',
168 'minTimeValue' => null,
169 'maxTimeValue' => null,
170 'limitTimeRange' => false,
171 'dateFieldType' => '',
172 'dateFormat' => '',
173 'minDateValue' => null,
174 'maxDateValue' => null,
175 'limitDateRange' => false,
176 'defaultValue' => '',
177 'limitMaxLength' => false,
178 'maxLength' => 255,
179 'labelPosition' => 'default',
180 'noDuplicates' => false,
181 'inputPrefix' => '',
182 'inputSuffix' => '',
183 'ratingIcon' => 'star',
184 'fieldOptions' => self::defaultFieldOptions($type),
185 'showValues' => in_array(
186 $type,
187 ['radio', 'checkbox', 'select', 'multi-select', 'product'],
188 true
189 ),
190 ];
191 }
192
193 /**
194 * @return array<int, array<string, mixed>>
195 */
196 public static function compoundChildFields(
197 string $type,
198 int $formId,
199 int $fieldIndex,
200 int $position
201 ): array {
202 $strings = self::builderStrings();
203
204 if ($type === 'name') {
205 return [
206 [
207 'id' => 0,
208 'formId' => $formId,
209 'fieldIndex' => $fieldIndex,
210 'type' => 'text',
211 'label' => $strings['first_name'] ?? __('First name', 'ivyforms'),
212 'required' => false,
213 'defaultValue' => '',
214 'placeholder' => '',
215 'position' => $position,
216 'parentId' => 0,
217 'hideLabel' => false,
218 'description' => '',
219 'requiredMessage' => '',
220 'settings' => wp_json_encode(['nameFieldType' => 'nameField1']) ?: '',
221 ],
222 [
223 'id' => 0,
224 'formId' => $formId,
225 'fieldIndex' => $fieldIndex,
226 'type' => 'text',
227 'label' => $strings['last_name'] ?? __('Last name', 'ivyforms'),
228 'required' => false,
229 'defaultValue' => '',
230 'placeholder' => '',
231 'position' => $position,
232 'parentId' => 0,
233 'hideLabel' => false,
234 'description' => '',
235 'requiredMessage' => '',
236 'settings' => wp_json_encode(['nameFieldType' => 'nameField2']) ?: '',
237 ],
238 ];
239 }
240
241 if ($type !== 'address') {
242 return [];
243 }
244
245 $addressParts = [
246 ['type' => 'streetAddress', 'label' => $strings['street_address'] ?? __('Street address', 'ivyforms')],
247 ['type' => 'addressLine2', 'label' => $strings['address_line_2'] ?? __('Address line 2', 'ivyforms')],
248 ['type' => 'city', 'label' => $strings['city'] ?? __('City', 'ivyforms')],
249 ['type' => 'state', 'label' => $strings['state'] ?? __('State / Province', 'ivyforms')],
250 ['type' => 'zip', 'label' => $strings['zip'] ?? __('Zip / Postal code', 'ivyforms')],
251 ['type' => 'country', 'label' => $strings['country'] ?? __('Country', 'ivyforms')],
252 ];
253
254 $childFields = [];
255 foreach ($addressParts as $addressPart) {
256 $childFields[] = [
257 'id' => 0,
258 'formId' => $formId,
259 'fieldIndex' => $fieldIndex,
260 'type' => 'text',
261 'label' => $addressPart['label'],
262 'required' => false,
263 'defaultValue' => '',
264 'placeholder' => '',
265 'position' => $position,
266 'parentId' => 0,
267 'hideLabel' => false,
268 'description' => '',
269 'requiredMessage' => '',
270 'visible' => true,
271 'settings' => wp_json_encode(['addressFieldType' => $addressPart['type']]) ?: '',
272 ];
273 }
274
275 return $childFields;
276 }
277
278 /**
279 * @param array<string, mixed> $input
280 * @return array<int, array{label: string, value: string}>
281 */
282 private static function resolveLikertRows(array $input): array
283 {
284 if (isset($input['likertRows']) && is_array($input['likertRows'])) {
285 return self::sanitizeLikertOptions($input['likertRows']);
286 }
287
288 if (!empty($input['likertSingleRow'])) {
289 return [
290 ['label' => '', 'value' => '_single'],
291 ];
292 }
293
294 return self::defaultLikertRows();
295 }
296
297 /**
298 * @param array<int, mixed> $options
299 * @return array<int, array{label: string, value: string}>
300 */
301 private static function sanitizeLikertOptions(array $options): array
302 {
303 $sanitized = [];
304
305 foreach ($options as $option) {
306 if (!is_array($option)) {
307 continue;
308 }
309
310 $label = sanitize_text_field((string) ($option['label'] ?? ''));
311 $value = sanitize_text_field((string) ($option['value'] ?? ''));
312
313 if ($label === '' && $value === '') {
314 continue;
315 }
316
317 $sanitized[] = [
318 'label' => $label,
319 'value' => $value !== '' ? $value : sanitize_title($label),
320 ];
321 }
322
323 return $sanitized;
324 }
325
326 /**
327 * @return array<int, array{label: string, value: string}>
328 */
329 private static function defaultLikertRows(): array
330 {
331 $strings = self::growthFieldOptionsStrings();
332
333 return [
334 [
335 'label' => $strings['statement_1'] ?? __('Statement 1', 'ivyforms'),
336 'value' => 'statement_1',
337 ],
338 [
339 'label' => $strings['statement_2'] ?? __('Statement 2', 'ivyforms'),
340 'value' => 'statement_2',
341 ],
342 [
343 'label' => $strings['statement_3'] ?? __('Statement 3', 'ivyforms'),
344 'value' => 'statement_3',
345 ],
346 ];
347 }
348
349 /**
350 * Likert rows are persisted as hidden child text fields (same pattern as the builder save).
351 *
352 * @param array<int, array{label: string, value: string}> $likertRows
353 * @return array<int, array<string, mixed>>
354 */
355 public static function likertRowChildFields(
356 array $likertRows,
357 int $formId,
358 int $fieldIndex,
359 int $position
360 ): array {
361 $childFields = [];
362
363 foreach ($likertRows as $rowIndex => $row) {
364 $childFields[] = [
365 'id' => 0,
366 'formId' => $formId,
367 'fieldIndex' => $fieldIndex,
368 'type' => 'text',
369 'label' => $row['label'],
370 'required' => false,
371 'defaultValue' => $row['value'],
372 'placeholder' => '',
373 'position' => $position,
374 'rowIndex' => $position,
375 'columnIndex' => 0,
376 'width' => 100,
377 'parentId' => 0,
378 'hideLabel' => false,
379 'readOnly' => false,
380 'description' => '',
381 'requiredMessage' => '',
382 'visible' => true,
383 'subFieldIndex' => $rowIndex,
384 ];
385 }
386
387 return $childFields;
388 }
389
390 /**
391 * @return array<string, string>
392 */
393 private static function growthFieldOptionsStrings(): array
394 {
395 if (!class_exists(\IvyFormsPro\Plans\Growth\Services\Translations\BackendStrings::class)) {
396 return [];
397 }
398
399 /** @var array<string, string> */
400 return \IvyFormsPro\Plans\Growth\Services\Translations\BackendStrings::getFieldOptionsStrings();
401 }
402
403 /**
404 * @return array<int, array<string, mixed>>
405 */
406 public static function defaultFieldOptions(string $type): array
407 {
408 $optionsByType = [
409 'radio' => [
410 [
411 'id' => 0,
412 'fieldId' => 0,
413 'label' => __('Option 1', 'ivyforms'),
414 'value' => 'option1',
415 'isDefault' => true,
416 'position' => 1,
417 ],
418 [
419 'id' => 0,
420 'fieldId' => 0,
421 'label' => __('Option 2', 'ivyforms'),
422 'value' => 'option2',
423 'isDefault' => false,
424 'position' => 2,
425 ],
426 ],
427 'checkbox' => [
428 [
429 'id' => 0,
430 'fieldId' => 0,
431 'label' => __('Choice 1', 'ivyforms'),
432 'value' => 'choice1',
433 'isDefault' => false,
434 'position' => 1,
435 ],
436 [
437 'id' => 0,
438 'fieldId' => 0,
439 'label' => __('Choice 2', 'ivyforms'),
440 'value' => 'choice2',
441 'isDefault' => false,
442 'position' => 2,
443 ],
444 [
445 'id' => 0,
446 'fieldId' => 0,
447 'label' => __('Choice 3', 'ivyforms'),
448 'value' => 'choice3',
449 'isDefault' => false,
450 'position' => 3,
451 ],
452 ],
453 'select' => [
454 [
455 'id' => 0,
456 'fieldId' => 0,
457 'label' => __('Option 1', 'ivyforms'),
458 'value' => 'option1',
459 'isDefault' => true,
460 'position' => 1,
461 ],
462 [
463 'id' => 0,
464 'fieldId' => 0,
465 'label' => __('Option 2', 'ivyforms'),
466 'value' => 'option2',
467 'isDefault' => false,
468 'position' => 2,
469 ],
470 ],
471 'multi-select' => [
472 [
473 'id' => 0,
474 'fieldId' => 0,
475 'label' => __('Option 1', 'ivyforms'),
476 'value' => 'option1',
477 'isDefault' => false,
478 'position' => 1,
479 ],
480 [
481 'id' => 0,
482 'fieldId' => 0,
483 'label' => __('Option 2', 'ivyforms'),
484 'value' => 'option2',
485 'isDefault' => false,
486 'position' => 2,
487 ],
488 [
489 'id' => 0,
490 'fieldId' => 0,
491 'label' => __('Option 3', 'ivyforms'),
492 'value' => 'option3',
493 'isDefault' => false,
494 'position' => 3,
495 ],
496 ],
497 ];
498
499 return $optionsByType[$type] ?? [];
500 }
501
502 /**
503 * Duplicate options for copied fields.
504 *
505 * @param array<int, array<string, mixed>> $fieldOptions
506 * @return array<int, array<string, mixed>>
507 */
508 public static function duplicateFieldOptions(array $fieldOptions): array
509 {
510 $duplicatedOptions = [];
511 $position = 1;
512
513 foreach ($fieldOptions as $fieldOption) {
514 $fieldOption['id'] = 0;
515 $fieldOption['fieldId'] = 0;
516 $fieldOption['position'] = $position++;
517 $duplicatedOptions[] = $fieldOption;
518 }
519
520 return $duplicatedOptions;
521 }
522
523 private static function defaultLabelForType(string $type): string
524 {
525 $builderStrings = self::builderStrings();
526 $commonStrings = self::commonStrings();
527 $componentStrings = self::componentStrings();
528
529 $labels = [
530 'text' => $builderStrings['text'] ?? __('Text', 'ivyforms'),
531 'textarea' => $builderStrings['paragraph'] ?? __('Paragraph', 'ivyforms'),
532 'email' => $builderStrings['email'] ?? __('Email', 'ivyforms'),
533 'number' => $builderStrings['number'] ?? __('Number', 'ivyforms'),
534 'phone' => $builderStrings['phone'] ?? __('Phone', 'ivyforms'),
535 'website' => $builderStrings['website'] ?? __('Website/URL', 'ivyforms'),
536 'name' => $builderStrings['name'] ?? __('Name', 'ivyforms'),
537 'address' => $builderStrings['address'] ?? __('Address', 'ivyforms'),
538 'radio' => $builderStrings['radio_button'] ?? __('Radio button', 'ivyforms'),
539 'checkbox' => $builderStrings['checkbox'] ?? __('Checkbox', 'ivyforms'),
540 'select' => $builderStrings['dropdown'] ?? __('Dropdown', 'ivyforms'),
541 'multi-select' => $builderStrings['dropdown'] ?? __('Dropdown', 'ivyforms'),
542 'date' => $builderStrings['date_picker'] ?? __('Date picker', 'ivyforms'),
543 'time' => $builderStrings['time_picker'] ?? __('Time picker', 'ivyforms'),
544 'date_time' => $builderStrings['date_time_picker'] ?? __('Date & time (picker)', 'ivyforms'),
545 'rating' => $builderStrings['rating'] ?? __('Rating', 'ivyforms'),
546 'html' => $builderStrings['html'] ?? __('HTML', 'ivyforms'),
547 'slider' => $builderStrings['slider'] ?? __('Slider', 'ivyforms'),
548 'file-upload' => $commonStrings['file_upload'] ?? __('File upload', 'ivyforms'),
549 'gdpr' => $builderStrings['gdpr'] ?? __('GDPR', 'ivyforms'),
550 'product' => $componentStrings['product'] ?? __('Product', 'ivyforms'),
551 'quantity' => $componentStrings['quantity'] ?? __('Quantity', 'ivyforms'),
552 'total' => $componentStrings['total'] ?? __('Total', 'ivyforms'),
553 'nps' => $builderStrings['net_promoter'] ?? __('Net promoter', 'ivyforms'),
554 'likert' => $builderStrings['likert'] ?? __('Likert', 'ivyforms'),
555 'rich_text' => $builderStrings['rich_text'] ?? __('Rich Text', 'ivyforms'),
556 'password' => $builderStrings['password'] ?? __('Password', 'ivyforms'),
557 'signature' => $builderStrings['signature'] ?? __('Signature', 'ivyforms'),
558 ];
559
560 return $labels[$type] ?? ucfirst(str_replace(['_', '-'], ' ', $type));
561 }
562
563 private static function defaultRequiredForType(string $type): bool
564 {
565 return in_array($type, ['gdpr', 'total'], true);
566 }
567
568 /**
569 * @return array<string, string>
570 */
571 private static function builderStrings(): array
572 {
573 /** @var array<string, string> */
574 return BackendStrings::getNewFormStrings();
575 }
576
577 /**
578 * @return array<string, string>
579 */
580 private static function commonStrings(): array
581 {
582 /** @var array<string, string> */
583 return BackendStrings::getCommonStrings();
584 }
585
586 /**
587 * @return array<string, string>
588 */
589 private static function componentStrings(): array
590 {
591 /** @var array<string, string> */
592 return BackendStrings::getComponentsStrings();
593 }
594 }
595