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 / Repository / Field / FieldRepository.php

FieldRepository.php in The Innovative Form Builder – IvyForms 1.5.1, at backend/src/Repository/Field/FieldRepository.php

426 lines 14.3 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 LICENCE.md for license details.
6 */
7
8 namespace IvyForms\Repository\Field;
9
10 use IvyForms\Common\Exceptions\InvalidArgumentException;
11 use IvyForms\Common\Exceptions\QueryExecutionException;
12 use IvyForms\Entity\Field\Field;
13 use IvyForms\Factory\Field\FieldFactory;
14 use IvyForms\Repository\AbstractRepository;
15 use IvyForms\Services\Translations\BackendStrings;
16
17 /**
18 * Class FieldRepository
19 *
20 * @package IvyForms\Repository\Field
21 */
22 class FieldRepository extends AbstractRepository implements FieldRepositoryInterface
23 {
24 public const FACTORY = FieldFactory::class;
25
26 /**
27 * Add field to the database
28 *
29 * @param Field $entity
30 *
31 * @return int
32 *
33 * @throws QueryExecutionException
34 */
35 public function add($entity): int
36 {
37
38 $data = $entity->toArray();
39
40 $result = $this->wpdb->insert(
41 $this->table,
42 [
43 'fieldIndex' => $data['fieldIndex'],
44 'formId' => $data['formId'],
45 'type' => $data['type'],
46 'label' => $data['label'],
47 'required' => (int)$data['required'],
48 'defaultValue' => $data['defaultValue'],
49 'placeholder' => $data['placeholder'],
50 'position' => $data['position'],
51 'rowIndex' => $data['rowIndex'] ?? 0,
52 'columnIndex' => $data['columnIndex'] ?? 0,
53 'width' => $data['width'] ?? 100,
54 'parentId' => $data['parentId'] ?? null,
55 'pageId' => $data['pageId'] ?? null,
56 'settings' => $this->encodeFieldSettings($data, $entity, false),
57 'dateCreated' => current_time('mysql')
58 ],
59 ['%d', '%d', '%s', '%s', '%d', '%s', '%s', '%d', '%d', '%d', '%d', '%d', '%s', '%s', '%s']
60 );
61
62 if ($result === false) {
63 throw new QueryExecutionException(
64 BackendStrings::getExceptionStrings()['unable_to_add_data'] . __CLASS__
65 );
66 }
67
68 return $this->wpdb->insert_id;
69 }
70
71 /**
72 * Update field in the database
73 *
74 * @param int $id
75 * @param Field $entity
76 *
77 * @return bool
78 *
79 * @throws QueryExecutionException
80 */
81 public function update(int $id, $entity): bool
82 {
83 if ($id < 1) {
84 throw new InvalidArgumentException('Field id must be a positive integer.');
85 }
86
87 $data = $entity->toArray();
88
89 $result = $this->wpdb->update(
90 $this->table,
91 [
92 'fieldIndex' => $data['fieldIndex'],
93 'formId' => $data['formId'],
94 'type' => $data['type'],
95 'label' => $data['label'],
96 'required' => (int)$data['required'],
97 'defaultValue' => $data['defaultValue'],
98 'placeholder' => $data['placeholder'],
99 'position' => $data['position'],
100 'rowIndex' => $data['rowIndex'] ?? 0,
101 'columnIndex' => $data['columnIndex'] ?? 0,
102 'width' => $data['width'] ?? 100,
103 'parentId' => $data['parentId'] ?? null,
104 'pageId' => $data['pageId'] ?? null,
105 'settings' => $this->encodeFieldSettings($data, $entity, true),
106 ],
107 ['id' => $id],
108 ['%d', '%d', '%s', '%s', '%d', '%s', '%s', '%d', '%d', '%d', '%d', '%d', '%s', '%s'],
109 ['%d']
110 );
111
112 if ($result === false) {
113 throw new QueryExecutionException(BackendStrings::getAllFormsStrings()['unable_to_save_data'] . __CLASS__);
114 }
115
116 return $result;
117 }
118
119 /**
120 * @param mixed[] $data
121 */
122 private function encodeFieldSettings(array $data, Field $entity, bool $forUpdate): string
123 {
124 $settings = $this->buildFieldSettingsPayload($data, $forUpdate);
125
126 /**
127 * Allows modification of the settings data before saving to database.
128 *
129 * @param mixed[] $settings The current settings array.
130 * @param mixed[] $data The full field data array.
131 * @param Field $entity The Field entity instance.
132 * @return mixed[] The modified settings array.
133 */
134 $settings = apply_filters('ivyforms/repository/field/settings', $settings, $data, $entity);
135
136 $encoded = wp_json_encode($settings);
137 if ($encoded === false) {
138 throw new QueryExecutionException(
139 'Failed to encode field settings: ' . json_last_error_msg()
140 );
141 }
142
143 return $encoded;
144 }
145
146 /**
147 * @param mixed[] $data
148 *
149 * @return mixed[]
150 */
151 private function buildFieldSettingsPayload(array $data, bool $forUpdate): array
152 {
153 return array_merge(
154 $this->buildCoreFieldSettings($data, $forUpdate),
155 $this->buildFileUploadFieldSettings($data)
156 );
157 }
158
159 /**
160 * @param mixed[] $data
161 *
162 * @return mixed[]
163 */
164 private function buildCoreFieldSettings(array $data, bool $forUpdate): array
165 {
166 return array_merge(
167 $this->buildBaseFieldSettings($data),
168 [
169 'limitRange' => $this->resolveLimitRangeFlag($data, $forUpdate),
170 'visible' => $this->resolveVisibleFlag($data, $forUpdate),
171 ],
172 $this->buildExtendedFieldSettings($data)
173 );
174 }
175
176 /**
177 * @param mixed[] $data
178 *
179 * @return mixed[]
180 */
181 private function buildBaseFieldSettings(array $data): array
182 {
183 return [
184 'hideLabel' => (int) $data['hideLabel'],
185 'readOnly' => (int) $data['readOnly'],
186 'description' => $data['description'],
187 'requiredMessage' => $data['requiredMessage'],
188 'cssClasses' => $data['cssClasses'],
189 'limitMaxLength' => (int) $data['limitMaxLength'],
190 'maxLength' => $data['maxLength'],
191 'labelPosition' => $data['labelPosition'],
192 'noDuplicates' => (int) $data['noDuplicates'],
193 'shuffleOptions' => (int) $data['shuffleOptions'],
194 'showValues' => (int) $data['showValues'],
195 'enableSearch' => (int) $data['enableSearch'],
196 'rows' => (int) $data['rows'],
197 ];
198 }
199
200 /**
201 * @param mixed[] $data
202 *
203 * @return mixed[]
204 */
205 private function buildExtendedFieldSettings(array $data): array
206 {
207 return [
208 'confirmFieldEnabled' => (int) ($data['confirmFieldEnabled'] ?? 0),
209 'confirmFieldLabel' => $data['confirmFieldLabel'] ?? '',
210 'confirmFieldPlaceholder' => $data['confirmFieldPlaceholder'] ?? '',
211 'confirmFieldHideLabel' => (int) ($data['confirmFieldHideLabel'] ?? 0),
212 'phoneFormat' => $data['phoneFormat'],
213 'phoneAutoDetect' => $data['phoneAutoDetect'],
214 'minValue' => isset($data['minValue']) ? (float) $data['minValue'] : null,
215 'maxValue' => isset($data['maxValue']) ? (float) $data['maxValue'] : null,
216 'step' => (float) $data['step'],
217 'showGrid' => (int) ($data['showGrid'] ?? 0),
218 'numberFormat' => $data['numberFormat'] ?? '',
219 'inputPrefix' => $data['inputPrefix'] ?? '',
220 'inputSuffix' => $data['inputSuffix'] ?? '',
221 'timeFieldType' => $data['timeFieldType'] ?? '',
222 'timeFormat' => $data['timeFormat'] ?? '',
223 'dateFieldType' => $data['dateFieldType'] ?? '',
224 'dateFormat' => $data['dateFormat'] ?? '',
225 'minDateValue' => $data['minDateValue'] ?? '',
226 'maxDateValue' => $data['maxDateValue'] ?? '',
227 'showRatingText' => (int) ($data['showRatingText'] ?? 0),
228 'ratingIcon' => $data['ratingIcon'] ?? 'star',
229 'htmlContent' => $data['htmlContent'] ?? '',
230 'agreementText' => $data['agreementText'] ?? '',
231 'limitDateRange' => (int) $data['limitDateRange'],
232 'minTimeValue' => $data['minTimeValue'] ?? '',
233 'maxTimeValue' => $data['maxTimeValue'] ?? '',
234 'limitTimeRange' => (int) $data['limitTimeRange'],
235 'nameFieldType' => $data['nameFieldType'] ?? '',
236 'subFieldIndex' => isset($data['subFieldIndex']) ? (int) $data['subFieldIndex'] : null,
237 ];
238 }
239
240 /**
241 * @param mixed[] $data
242 */
243 private function resolveLimitRangeFlag(array $data, bool $forUpdate): int
244 {
245 if (!$forUpdate) {
246 return (int) $data['limitRange'];
247 }
248
249 return filter_var($data['limitRange'], FILTER_VALIDATE_BOOLEAN) ? 1 : 0;
250 }
251
252 /**
253 * @param mixed[] $data
254 */
255 private function resolveVisibleFlag(array $data, bool $forUpdate): int
256 {
257 if (!$forUpdate) {
258 return (int) $data['visible'];
259 }
260
261 return array_key_exists('visible', $data) ? (int) $data['visible'] : 1;
262 }
263
264 /**
265 * @param mixed[] $data
266 *
267 * @return mixed[]
268 */
269 private function buildFileUploadFieldSettings(array $data): array
270 {
271 return [
272 'maxFileSizeMb' => isset($data['maxFileSizeMb']) ? (int) $data['maxFileSizeMb'] : 0,
273 'saveUploadsTo' => (string) ($data['saveUploadsTo'] ?? 'ivyforms'),
274 'allowedFileExtensions' => isset($data['allowedFileExtensions'])
275 && is_array($data['allowedFileExtensions'])
276 ? array_values($data['allowedFileExtensions'])
277 : [],
278 ];
279 }
280
281 /**
282 * Get fields by form ID
283 *
284 * @param int $id
285 *
286 * @return mixed[]
287 * @throws QueryExecutionException
288 */
289 public function getFieldsById(int $id): array
290 {
291 $rows = $this->wpdb->get_results(
292 $this->wpdb->prepare(
293 $this->selectQuery() . " WHERE $this->table.formId = %d ORDER BY $this->table.position ASC",
294 $id
295 ),
296 ARRAY_A
297 );
298 if ($rows === false) {
299 throw new QueryExecutionException(
300 BackendStrings::getExceptionStrings()['unable_find_by_id'] . __CLASS__
301 );
302 }
303 $result = [];
304 foreach ($rows as $row) {
305 if (array_key_exists('parentId', $row)) {
306 $row['parentId'] = $row['parentId'] !== null ? (int)$row['parentId'] : null;
307 }
308 $result[] = call_user_func([static::FACTORY, 'create'], $row);
309 }
310 return $result;
311 }
312
313 /**
314 * Get multiple fields by their IDs in a single query.
315 *
316 * @param int[] $fieldIds
317 * @return array<int, object> Indexed by field ID
318 * @throws QueryExecutionException
319 */
320 public function getFieldsByIds(array $fieldIds): array
321 {
322 if (empty($fieldIds)) {
323 return [];
324 }
325
326 $fieldIds = array_map('intval', $fieldIds);
327 $placeholders = implode(',', array_fill(0, count($fieldIds), '%d'));
328 $rows = $this->wpdb->get_results(
329 $this->wpdb->prepare(
330 $this->selectQuery() . " WHERE $this->table.id IN ($placeholders) ORDER BY $this->table.position ASC",
331 ...$fieldIds
332 ),
333 ARRAY_A
334 );
335
336 if ($rows === false) {
337 throw new QueryExecutionException(
338 BackendStrings::getExceptionStrings()['unable_find_by_id'] . __CLASS__
339 );
340 }
341
342 $result = [];
343 foreach ($rows as $row) {
344 if (array_key_exists('parentId', $row)) {
345 $row['parentId'] = $row['parentId'] !== null ? (int)$row['parentId'] : null;
346 }
347 $field = call_user_func([static::FACTORY, 'create'], $row);
348 $result[$field->getId()] = $field;
349 }
350
351 return $result;
352 }
353
354 /**
355 * Delete multiple fields by their IDs in bulk, and their field options.
356 *
357 * @param array<int> $ids
358 * @return int Number of deleted fields
359 * @throws QueryExecutionException
360 */
361 public function deleteMany(array $ids): int
362 {
363 if (empty($ids)) {
364 return 0;
365 }
366 $placeholders = implode(',', array_fill(0, count($ids), '%d'));
367 $deleteFieldsQuery = "DELETE FROM {$this->table} WHERE id IN ($placeholders)";
368 $result = $this->wpdb->query($this->wpdb->prepare($deleteFieldsQuery, ...$ids));
369 if ($result === false) {
370 throw new QueryExecutionException(
371 sprintf(
372 BackendStrings::getExceptionStrings()['failed_delete_by_ids'],
373 implode(', ', $ids),
374 $this->table
375 )
376 );
377 }
378 return $result;
379 }
380
381 /**
382 * Delete multiple fields by foreign key values (formId), and their field options.
383 *
384 * @param array<int> $formIds
385 * @param string $foreignColumn
386 * @return int Number of deleted fields
387 * @throws QueryExecutionException
388 */
389 public function deleteManyByForeignKeyValues(array $formIds, string $foreignColumn = 'formId'): int
390 {
391 if (empty($formIds)) {
392 return 0;
393 }
394 $formIds = array_map('intval', $formIds);
395 $placeholders = implode(',', array_fill(0, count($formIds), '%d'));
396 $deleteFieldsQuery = "DELETE FROM {$this->table} WHERE {$foreignColumn} IN ($placeholders)";
397 $result = $this->wpdb->query($this->wpdb->prepare($deleteFieldsQuery, ...$formIds));
398 if ($result === false) {
399 throw new QueryExecutionException(
400 BackendStrings::getExceptionStrings()['failed_delete_by_foreign_keys']
401 );
402 }
403 return $result;
404 }
405
406 /**
407 * Delete fields by a single foreign key value (formId), and their field options.
408 *
409 * @param int $formId
410 * @param string $foreignColumn
411 * @return int Number of deleted fields
412 * @throws QueryExecutionException
413 */
414 public function deleteOneByForeignKeyValue(int $formId, string $foreignColumn = 'formId'): int
415 {
416 $deleteFieldsQuery = "DELETE FROM {$this->table} WHERE {$foreignColumn} = %d";
417 $result = $this->wpdb->query($this->wpdb->prepare($deleteFieldsQuery, $formId));
418 if ($result === false) {
419 throw new QueryExecutionException(
420 BackendStrings::getExceptionStrings()['failed_delete_by_foreign_key']
421 );
422 }
423 return $result;
424 }
425 }
426