PluginProbe
Fluent Forms – Customizable Contact Forms, Survey, Quiz, & Conversational Form Builder / 6.2.14
Fluent Forms – Customizable Contact Forms, Survey, Quiz, & Conversational Form Builder v6.2.14
6.2.14 6.2.13 6.2.12 6.2.10 6.2.11 6.2.9 6.2.8 6.2.7 6.2.6 6.2.5 6.2.4 6.2.3 6.2.2 3.6.22 3.6.31 3.6.40 3.6.41 3.6.42 3.6.50 3.6.51 3.6.60 3.6.61 3.6.62 3.6.64 3.6.65 All 196 releases
fluentform / app / Modules / MCP / Support / FormCreator.php

FormCreator.php in Fluent Forms – Customizable Contact Forms, Survey, Quiz, & Conversational Form Builder 6.2.14, at app/Modules/MCP/Support/FormCreator.php

171 lines 6.1 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2
3 namespace FluentForm\App\Modules\MCP\Support;
4
5 defined('ABSPATH') || exit;
6
7 use FluentForm\App\Modules\Acl\Acl;
8 use FluentForm\App\Modules\Ai\AiFormBuilder;
9 use FluentForm\App\Services\Form\FormService;
10 use FluentForm\Framework\Support\Arr;
11
12 /**
13 * Builds a form from a simple field spec for the create-form MCP tool.
14 *
15 * Extends AiFormBuilder purely to reuse its (protected) field-mapping + save
16 * pipeline, so the MCP concern stays out of the AI builder itself.
17 */
18 class FormCreator extends AiFormBuilder
19 {
20 /**
21 * Set up the save pipeline without AiFormBuilder's constructor, which only
22 * adds it to register an admin-ajax handler this REST/MCP path never uses.
23 *
24 * KNOWN COUPLING (accepted): naming the grandparent explicitly is only legal
25 * because AiFormBuilder extends FormService, so this skips exactly one level.
26 * If that chain ever changes, PHP will fatal here rather than fail quietly —
27 * which is the reason this is written out rather than hidden behind
28 * parent::__construct(), whose side effect is the thing being avoided.
29 * Not worked around further because the alternative is editing
30 * AiFormBuilder, which this PR deliberately leaves untouched.
31 */
32 public function __construct()
33 {
34 FormService::__construct();
35 }
36
37 /**
38 * Create a form from a title + field spec.
39 *
40 * @param array $form { title: string, fields: array, is_conversational?: bool }.
41 * @return \FluentForm\App\Models\Form
42 * @throws \Exception
43 */
44 public function create(array $form)
45 {
46 Acl::verify('fluentform_forms_manager');
47
48 $form['fields'] = $this->assignStorageNames($this->normalizeSpec(Arr::get($form, 'fields', [])));
49
50 return $this->prepareAndSaveForm($form);
51 }
52
53 /**
54 * Turn a simple field spec into FluentForm's formatted `fields` array — the
55 * same mapping create() runs, minus the save. Reuses the AI builder's
56 * per-element processing so an edited form matches a created one. Multi-step
57 * (form_step) elements are skipped; the field-edit tool preserves the existing
58 * form's step structure separately.
59 *
60 * @param array $fields Simple field spec (list of {element/attributes/settings}).
61 * @return array Formatted fields ready to slot into form_fields['fields'].
62 */
63 public function formatFields(array $fields)
64 {
65 $fields = $this->assignStorageNames($this->normalizeSpec($fields));
66 $allFields = $this->getDefaultFields();
67 $disabled = array_keys($this->getDisabledComponents());
68
69 $formatted = [];
70 foreach ($fields as $field) {
71 if (is_array($field) && 1 === count($field)) {
72 $field = reset($field);
73 }
74 $element = $this->resolveInput($field);
75 if (!$element || 'form_step' === $element || in_array($element, $disabled, true)) {
76 continue;
77 }
78 $formatted[] = $this->processField($element, $field, $allFields);
79 }
80
81 return array_values(array_filter($formatted));
82 }
83
84 /**
85 * Assign a unique attributes.name to every input field. Submission responses
86 * are keyed by field name, and AiFormBuilder keeps the component default when
87 * no name is supplied — so two fields of the same type would share one
88 * storage key and overwrite each other's submitted values. Idempotent:
89 * fields that already carry a unique name pass through unchanged, so the
90 * tool boundary can assign names and create() re-running it is a no-op.
91 */
92 public function assignStorageNames(array $fields)
93 {
94 $used = [];
95
96 foreach ($fields as &$field) {
97 if (!is_array($field)) {
98 continue;
99 }
100
101 $element = $this->resolveInput($field);
102 if (!$element || 'container' === $element) {
103 continue;
104 }
105
106 $current = Arr::get($field, 'attributes.name');
107 $default = Arr::get($this->getDefaultFields(), $element . '.attributes.name');
108 if (!$current && !$default) {
109 // Element has no storage key (custom_html, section_break, …).
110 continue;
111 }
112
113 $base = $current ? $current : $this->nameFromLabel(Arr::get($field, 'settings.label', ''), $default ? $default : $element);
114
115 $name = $base;
116 $count = 1;
117 while (isset($used[$name])) {
118 $name = $base . '_' . $count;
119 ++$count;
120 }
121 $used[$name] = true;
122
123 $field['attributes']['name'] = $name;
124 }
125 unset($field);
126
127 return $fields;
128 }
129
130 /**
131 * Lift a top-level `label` (the simple shape both create-form and
132 * update-form-fields accept) into settings.label, where assignStorageNames and
133 * the builder read it. Without this, update-form-fields' {type,label} spec
134 * lost its label, field names fell back to element defaults, and re-sending a
135 * form's own fields read as a full removal. Idempotent: an explicit
136 * settings.label is left untouched, so the full {element,attributes,settings}
137 * shape passes through unchanged.
138 */
139 private function normalizeSpec(array $fields)
140 {
141 foreach ($fields as &$field) {
142 if (!is_array($field)) {
143 continue;
144 }
145 $label = Arr::get($field, 'label');
146 if ('' === (string) $label || null !== Arr::get($field, 'settings.label')) {
147 continue;
148 }
149 $label = sanitize_text_field($label);
150 $field['settings']['label'] = $label;
151 if (null === Arr::get($field, 'settings.admin_field_label')) {
152 $field['settings']['admin_field_label'] = $label;
153 }
154 }
155 unset($field);
156
157 return $fields;
158 }
159
160 private function nameFromLabel($label, $fallback)
161 {
162 $name = sanitize_key(str_replace([' ', '-'], '_', (string) $label));
163 $name = preg_replace('/_+/', '_', trim($name, '_'));
164 if (strlen($name) > 40) {
165 $name = rtrim(substr($name, 0, 40), '_');
166 }
167
168 return '' !== $name ? $name : $fallback;
169 }
170 }
171