PluginProbe
The Innovative Form Builder – IvyForms / 1.1
The Innovative Form Builder – IvyForms v1.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 / Factory / Field / FieldFactory.php

FieldFactory.php in The Innovative Form Builder – IvyForms 1.1, at backend/src/Factory/Field/FieldFactory.php

345 lines 14.8 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2
3 namespace IvyForms\Factory\Field;
4
5 use IvyForms\Common\Exceptions\ValidationException;
6 use IvyForms\Entity\Field\Field as FieldEntity;
7 use IvyForms\ValueObjects\Field\Field;
8 use IvyForms\ValueObjects\Field\FieldGeneralSettings;
9 use IvyForms\ValueObjects\Field\FieldOptions;
10 use IvyForms\ValueObjects\Field\FieldAdvancedSettings;
11 use IvyForms\ValueObjects\Field\ConfirmationGeneralSettings;
12 use IvyForms\ValueObjects\Field\TimeFieldGeneralSettings;
13 use IvyForms\ValueObjects\Field\DateFieldGeneralSettings;
14
15 /**
16 * Class FieldFactory
17 *
18 * @package IvyForms\Factory\Field
19 */
20 class FieldFactory
21 {
22 /**
23 * @param array<string, mixed> $data
24 *
25 * @return FieldEntity
26 *
27 * @throws ValidationException
28 */
29 public static function create(array $data): FieldEntity
30 {
31 $settings = isset($data['settings']) ? json_decode($data['settings'], true) : [];
32
33 if (!empty($settings)) {
34 $data['hideLabel'] = $settings['hideLabel'] ?? false;
35 $data['readOnly'] = $settings['readOnly'] ?? false;
36 $data['description'] = $settings['description'] ?? '';
37 $data['requiredMessage'] = $settings['requiredMessage'] ?? '';
38 $data['cssClasses'] = $settings['cssClasses'] ?? '';
39 $data['limitMaxLength'] = $settings['limitMaxLength'] ?? false;
40 $data['limitRange'] = $settings['limitRange'] ?? false;
41 $data['maxLength'] = $settings['maxLength'] ?? 255;
42 $data['labelPosition'] = $settings['labelPosition'] ?? 'default';
43 $data['noDuplicates'] = $settings['noDuplicates'] ?? false;
44 $data['shuffleOptions'] = $settings['shuffleOptions'] ?? false;
45 $data['showValues'] = $settings['showValues'] ?? false;
46 $data['enableSearch'] = $settings['enableSearch'] ?? false;
47 $data['rows'] = $settings['rows'] ?? 0;
48 $data['confirmFieldEnabled'] = $settings['confirmFieldEnabled'] ?? false;
49 $data['confirmFieldLabel'] = $settings['confirmFieldLabel'] ?? '';
50 $data['confirmFieldPlaceholder'] = $settings['confirmFieldPlaceholder'] ?? '';
51 $data['confirmFieldHideLabel'] = $settings['confirmFieldHideLabel'] ?? false;
52 $data['phoneFormat'] = $settings['phoneFormat'] ?? '';
53 $data['phoneAutoDetect'] = $settings['phoneAutoDetect'] ?? false;
54 $data['minValue'] = $settings['minValue'] ?? null;
55 $data['maxValue'] = $settings['maxValue'] ?? null;
56 $data['step'] = $settings['step'] ?? 1;
57 $data['showGrid'] = $settings['showGrid'] ?? false;
58 $data['numberFormat'] = $settings['numberFormat'] ?? '';
59 $data['inputPrefix'] = $settings['inputPrefix'] ?? '';
60 $data['inputSuffix'] = $settings['inputSuffix'] ?? '';
61 $data['timeFieldType'] = $settings['timeFieldType'] ?? '';
62 $data['timeFormat'] = $settings['timeFormat'] ?? '';
63 $data['minTimeValue'] = $settings['minTimeValue'] ?? '';
64 $data['maxTimeValue'] = $settings['maxTimeValue'] ?? '';
65 $data['dateFieldType'] = $settings['dateFieldType'] ?? '';
66 $data['dateFormat'] = $settings['dateFormat'] ?? '';
67 $data['minDateValue'] = $settings['minDateValue'] ?? '';
68 $data['maxDateValue'] = $settings['maxDateValue'] ?? '';
69 $data['showRatingText'] = $settings['showRatingText'] ?? false;
70 $data['ratingIcon'] = $settings['ratingIcon'] ?? 'star';
71 $data['htmlContent'] = $settings['htmlContent'] ?? '';
72 $data['agreementText'] = $settings['agreementText'] ?? '';
73 $data['limitDateRange'] = $settings['limitDateRange'] ?? false;
74 $data['limitTimeRange'] = $settings['limitTimeRange'] ?? false;
75 // Grid layout: prioritize direct database columns over settings JSON
76 // This ensures backward compatibility while preferring the dedicated columns
77 $data['rowIndex'] = $data['rowIndex'] ?? ($settings['rowIndex'] ?? 0);
78 $data['columnIndex'] = $data['columnIndex'] ?? ($settings['columnIndex'] ?? 0);
79 $data['width'] = $data['width'] ?? ($settings['width'] ?? 100);
80 $data['visible'] = array_key_exists('visible', $settings)
81 ? filter_var($settings['visible'], FILTER_VALIDATE_BOOLEAN, FILTER_NULL_ON_FAILURE)
82 : true;
83 if ($data['visible'] === null) {
84 $data['visible'] = true;
85 }
86 }
87
88 /**
89 * Allows modification of the settings data before creating the Field entity.
90 * This filter runs for both:
91 * - Fields loaded from database (settings from JSON string)
92 * - Fields from API (settings from direct properties)
93 * @since 0.1.0
94 *
95 * @param mixed[] $data The current field data.
96 * @param mixed[] $settings The settings array (from database or empty for API).
97 * @return mixed[] The modified field data.
98 */
99 $data = apply_filters('ivyforms/field/factory/extract_settings', $data, $settings);
100 $generalSettings = new FieldGeneralSettings(
101 self::validateString($data['label'] ?? '', 255, 'label'),
102 self::validateBool($data['required'] ?? false),
103 self::validateString($data['placeholder'] ?? '', 255, 'placeholder'),
104 self::validateBool($data['hideLabel'] ?? false),
105 self::validateBool($data['readOnly'] ?? false),
106 self::validateString($data['description'] ?? '', 1000, 'description'),
107 self::validateString($data['requiredMessage'] ?? '', 255, 'requiredMessage'),
108 self::validateString($data['cssClasses'] ?? '', 255, 'cssClasses'),
109 self::validateBool($data['shuffleOptions'] ?? false),
110 self::validateBool($data['showValues'] ?? false),
111 self::validateBool($data['enableSearch'] ?? false),
112 self::validatePhoneFormat($data['phoneFormat'] ?? ''),
113 self::validateBool($data['phoneAutoDetect'] ?? false),
114 self::validateNumberValue($data['minValue'] ?? null, 'minValue'),
115 self::validateNumberValue($data['maxValue'] ?? null, 'maxValue'),
116 self::validateStep($data['step'] ?? 1),
117 self::validateBool($data['showGrid'] ?? false),
118 self::validateNumberFormat($data['numberFormat'] ?? ''),
119 new ConfirmationGeneralSettings(
120 self::validateBool($data['confirmFieldEnabled'] ?? false),
121 self::validateString(
122 $data['confirmFieldLabel'] ?? '',
123 255,
124 'confirmFieldLabel'
125 ),
126 self::validateString(
127 $data['confirmFieldPlaceholder'] ?? '',
128 255,
129 'confirmFieldPlaceholder'
130 ),
131 self::validateBool($data['confirmFieldHideLabel'] ?? false)
132 ),
133 new TimeFieldGeneralSettings(
134 self::validateBool($data['limitTimeRange'] ?? false),
135 self::validateString($data['timeFieldType'] ?? '', 50, 'timeFieldType'),
136 self::validateString($data['timeFormat'] ?? '', 50, 'timeFormat'),
137 isset($data['minTimeValue']) && $data['minTimeValue'] !== '' ?
138 self::validateString($data['minTimeValue'], 255, 'minTimeValue') : null,
139 isset($data['maxTimeValue']) && $data['maxTimeValue'] !== '' ?
140 self::validateString($data['maxTimeValue'], 255, 'maxTimeValue') : null,
141 ),
142 new DateFieldGeneralSettings(
143 self::validateBool($data['limitDateRange'] ?? false),
144 self::validateString($data['dateFieldType'] ?? '', 50, 'dateFieldType'),
145 self::validateString($data['dateFormat'] ?? '', 50, 'dateFormat'),
146 isset($data['minDateValue']) && $data['minDateValue'] !== '' ?
147 self::validateString($data['minDateValue'], 255, 'minDateValue') : null,
148 isset($data['maxDateValue']) && $data['maxDateValue'] !== '' ?
149 self::validateString($data['maxDateValue'], 255, 'maxDateValue') : null,
150 ),
151 self::validateBool($data['showRatingText'] ?? false),
152 self::validateBool($data['visible'] ?? true),
153 self::validateString($data['htmlContent'] ?? '', 4294967295, 'htmlContent'),
154 self::validateString($data['agreementText'] ?? '', 1000, 'agreementText'),
155 self::validateBool($data['limitRange'] ?? false),
156 );
157 $advancedSettings = new FieldAdvancedSettings(
158 self::validateString($data['defaultValue'] ?? '', 255, 'defaultValue'),
159 self::validateBool($data['limitMaxLength'] ?? false),
160 (int)($data['maxLength'] ?? 255),
161 self::validateString($data['labelPosition'] ?? '', 255, 'labelPosition'),
162 self::validateBool($data['noDuplicates'] ?? false),
163 self::validateString($data['inputPrefix'] ?? '', 255, 'inputPrefix'),
164 self::validateString($data['inputSuffix'] ?? '', 255, 'inputSuffix'),
165 self::validateString($data['ratingIcon'] ?? 'star', 50, 'ratingIcon')
166 );
167 $fieldOptions = new FieldOptions($data['options'] ?? []);
168
169 $fieldObject = new Field(
170 $data['id'] ?? 0,
171 $data['formId'] ?? 0,
172 $data['fieldIndex'] ?? 0,
173 $data['type'] ?? '',
174 $data['position'] ?? 0,
175 $generalSettings,
176 $fieldOptions,
177 $advancedSettings,
178 $data['parentId'] ?? null,
179 $data['rows'] ?? 0,
180 $data['rowIndex'] ?? 0,
181 $data['columnIndex'] ?? 0,
182 $data['width'] ?? 100,
183 $data['pageId'] ?? null
184 );
185
186 /**
187 * Allows setting of additional properties for the Field value object.
188 * @since 0.1.0
189 *
190 * @param Field $fieldObject The current field data.
191 * @param mixed[] $data The settings array from the database.
192 * @return mixed[] The modified field data.
193 */
194 $fieldObject = apply_filters('ivyforms/field/value_object/set_properties', $fieldObject, $data);
195
196 $field = new FieldEntity($fieldObject);
197
198 if (isset($data['id'])) {
199 $field->setId($data['id']);
200 }
201
202 if (isset($data['formId'])) {
203 $field->setFormId($data['formId']);
204 }
205
206 if (isset($data['fieldIndex'])) {
207 $field->setIndex($data['fieldIndex']);
208 }
209
210 if (isset($data['type'])) {
211 $field->setType($data['type']);
212 }
213
214 if (isset($data['position'])) {
215 $field->setPosition($data['position']);
216 }
217
218 if (isset($data['parentId'])) {
219 $field->setParentId($data['parentId']);
220 }
221
222 if (isset($data['pageId'])) {
223 $field->setPageId($data['pageId']);
224 }
225
226 if (isset($data['rows'])) {
227 $field->setRows((int)$data['rows']);
228 }
229
230 /**
231 * Allows setting of additional properties for the Field entity.
232 * @since 0.1.0
233 *
234 * @param FieldEntity $field The current field data.
235 * @param mixed[] $data The settings array from the database.
236 * @return mixed[] The modified field data.
237 */
238 return apply_filters('ivyforms/field/entity/set_properties', $field, $data);
239 }
240
241 /**
242 * Validate and convert a value to boolean.
243 *
244 * @param mixed $value The value to validate.
245 *
246 * @return bool The validated boolean value.
247 */
248 private static function validateBool($value): bool
249 {
250 return (bool)$value;
251 }
252
253 /**
254 * Validate a string's length.
255 *
256 * @param mixed $value The value to validate.
257 * @param int $maxLength The maximum allowed length.
258 * @param string $fieldName The name of the field (for error messages).
259 *
260 * @return string The validated string.
261 *
262 * @throws ValidationException If the string exceeds the maximum length.
263 */
264 private static function validateString($value, int $maxLength, string $fieldName): string
265 {
266 $value = (string)$value;
267 if (strlen($value) > $maxLength) {
268 throw new ValidationException(
269 sprintf(
270 esc_html__('%1$s must be at most %2$d characters.', 'ivyforms'),
271 esc_html($fieldName),
272 $maxLength
273 )
274 );
275 }
276 return $value;
277 }
278 /**
279 * Validate and coerce numeric range values (minValue / maxValue).
280 * If value is null/empty/non-numeric => returns 0.0.
281 * Performs cross-check when the counterpart is already set.
282 *
283 * @param mixed $value
284 * @param string $fieldName 'minValue' | 'maxValue'
285 * @return float | null
286 * @throws ValidationException
287 */
288 private static function validateNumberValue($value, string $fieldName): ?float
289 {
290 if ($value === '' || $value === null) {
291 return null;
292 }
293 if (!is_numeric($value)) {
294 throw new ValidationException(
295 sprintf(
296 esc_html__('%1$s must be a number.', 'ivyforms'),
297 esc_html($fieldName)
298 )
299 );
300 }
301 return (float)$value;
302 }
303
304 /**
305 * Validate step (positive number >0)
306 * @param float $value
307 * @return float
308 * @throws ValidationException
309 */
310 private static function validateStep(float $value): float
311 {
312 if ($value <= 0) {
313 throw new ValidationException(esc_html__('step must be a positive number (>0).', 'ivyforms'));
314 }
315 return (float)$value;
316 }
317
318 /**
319 * Validates the number format.
320 *
321 * @param string $value
322 *
323 * @return string
324 *
325 */
326 private static function validateNumberFormat(string $value): string
327 {
328 $allowed = ['', 'us_decimal', 'us_int', 'eu_decimal', 'eu_int'];
329 if (!in_array($value, $allowed, true)) {
330 return '';
331 }
332 return $value;
333 }
334
335 private static function validatePhoneFormat(string $format): string
336 {
337 $allowed = ['international','national','e164'];
338 $lower = strtolower($format);
339 if (!in_array($lower, $allowed, true)) {
340 return 'international';
341 }
342 return $lower;
343 }
344 }
345