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

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

424 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 [
167 ...$this->buildBaseFieldSettings($data),
168 'limitRange' => $this->resolveLimitRangeFlag($data, $forUpdate),
169 'visible' => $this->resolveVisibleFlag($data, $forUpdate),
170 ...$this->buildExtendedFieldSettings($data),
171 ];
172 }
173
174 /**
175 * @param mixed[] $data
176 *
177 * @return mixed[]
178 */
179 private function buildBaseFieldSettings(array $data): array
180 {
181 return [
182 'hideLabel' => (int) $data['hideLabel'],
183 'readOnly' => (int) $data['readOnly'],
184 'description' => $data['description'],
185 'requiredMessage' => $data['requiredMessage'],
186 'cssClasses' => $data['cssClasses'],
187 'limitMaxLength' => (int) $data['limitMaxLength'],
188 'maxLength' => $data['maxLength'],
189 'labelPosition' => $data['labelPosition'],
190 'noDuplicates' => (int) $data['noDuplicates'],
191 'shuffleOptions' => (int) $data['shuffleOptions'],
192 'showValues' => (int) $data['showValues'],
193 'enableSearch' => (int) $data['enableSearch'],
194 'rows' => (int) $data['rows'],
195 ];
196 }
197
198 /**
199 * @param mixed[] $data
200 *
201 * @return mixed[]
202 */
203 private function buildExtendedFieldSettings(array $data): array
204 {
205 return [
206 'confirmFieldEnabled' => (int) ($data['confirmFieldEnabled'] ?? 0),
207 'confirmFieldLabel' => $data['confirmFieldLabel'] ?? '',
208 'confirmFieldPlaceholder' => $data['confirmFieldPlaceholder'] ?? '',
209 'confirmFieldHideLabel' => (int) ($data['confirmFieldHideLabel'] ?? 0),
210 'phoneFormat' => $data['phoneFormat'],
211 'phoneAutoDetect' => $data['phoneAutoDetect'],
212 'minValue' => isset($data['minValue']) ? (float) $data['minValue'] : null,
213 'maxValue' => isset($data['maxValue']) ? (float) $data['maxValue'] : null,
214 'step' => (float) $data['step'],
215 'showGrid' => (int) ($data['showGrid'] ?? 0),
216 'numberFormat' => $data['numberFormat'] ?? '',
217 'inputPrefix' => $data['inputPrefix'] ?? '',
218 'inputSuffix' => $data['inputSuffix'] ?? '',
219 'timeFieldType' => $data['timeFieldType'] ?? '',
220 'timeFormat' => $data['timeFormat'] ?? '',
221 'dateFieldType' => $data['dateFieldType'] ?? '',
222 'dateFormat' => $data['dateFormat'] ?? '',
223 'minDateValue' => $data['minDateValue'] ?? '',
224 'maxDateValue' => $data['maxDateValue'] ?? '',
225 'showRatingText' => (int) ($data['showRatingText'] ?? 0),
226 'ratingIcon' => $data['ratingIcon'] ?? 'star',
227 'htmlContent' => $data['htmlContent'] ?? '',
228 'agreementText' => $data['agreementText'] ?? '',
229 'limitDateRange' => (int) $data['limitDateRange'],
230 'minTimeValue' => $data['minTimeValue'] ?? '',
231 'maxTimeValue' => $data['maxTimeValue'] ?? '',
232 'limitTimeRange' => (int) $data['limitTimeRange'],
233 'nameFieldType' => $data['nameFieldType'] ?? '',
234 'subFieldIndex' => isset($data['subFieldIndex']) ? (int) $data['subFieldIndex'] : null,
235 ];
236 }
237
238 /**
239 * @param mixed[] $data
240 */
241 private function resolveLimitRangeFlag(array $data, bool $forUpdate): int
242 {
243 if (!$forUpdate) {
244 return (int) $data['limitRange'];
245 }
246
247 return filter_var($data['limitRange'], FILTER_VALIDATE_BOOLEAN) ? 1 : 0;
248 }
249
250 /**
251 * @param mixed[] $data
252 */
253 private function resolveVisibleFlag(array $data, bool $forUpdate): int
254 {
255 if (!$forUpdate) {
256 return (int) $data['visible'];
257 }
258
259 return array_key_exists('visible', $data) ? (int) $data['visible'] : 1;
260 }
261
262 /**
263 * @param mixed[] $data
264 *
265 * @return mixed[]
266 */
267 private function buildFileUploadFieldSettings(array $data): array
268 {
269 return [
270 'maxFileSizeMb' => isset($data['maxFileSizeMb']) ? (int) $data['maxFileSizeMb'] : 0,
271 'saveUploadsTo' => (string) ($data['saveUploadsTo'] ?? 'ivyforms'),
272 'allowedFileExtensions' => isset($data['allowedFileExtensions'])
273 && is_array($data['allowedFileExtensions'])
274 ? array_values($data['allowedFileExtensions'])
275 : [],
276 ];
277 }
278
279 /**
280 * Get fields by form ID
281 *
282 * @param int $id
283 *
284 * @return mixed[]
285 * @throws QueryExecutionException
286 */
287 public function getFieldsById(int $id): array
288 {
289 $rows = $this->wpdb->get_results(
290 $this->wpdb->prepare(
291 $this->selectQuery() . " WHERE $this->table.formId = %d ORDER BY $this->table.position ASC",
292 $id
293 ),
294 ARRAY_A
295 );
296 if ($rows === false) {
297 throw new QueryExecutionException(
298 BackendStrings::getExceptionStrings()['unable_find_by_id'] . __CLASS__
299 );
300 }
301 $result = [];
302 foreach ($rows as $row) {
303 if (array_key_exists('parentId', $row)) {
304 $row['parentId'] = $row['parentId'] !== null ? (int)$row['parentId'] : null;
305 }
306 $result[] = call_user_func([static::FACTORY, 'create'], $row);
307 }
308 return $result;
309 }
310
311 /**
312 * Get multiple fields by their IDs in a single query.
313 *
314 * @param int[] $fieldIds
315 * @return array<int, object> Indexed by field ID
316 * @throws QueryExecutionException
317 */
318 public function getFieldsByIds(array $fieldIds): array
319 {
320 if (empty($fieldIds)) {
321 return [];
322 }
323
324 $fieldIds = array_map('intval', $fieldIds);
325 $placeholders = implode(',', array_fill(0, count($fieldIds), '%d'));
326 $rows = $this->wpdb->get_results(
327 $this->wpdb->prepare(
328 $this->selectQuery() . " WHERE $this->table.id IN ($placeholders) ORDER BY $this->table.position ASC",
329 ...$fieldIds
330 ),
331 ARRAY_A
332 );
333
334 if ($rows === false) {
335 throw new QueryExecutionException(
336 BackendStrings::getExceptionStrings()['unable_find_by_id'] . __CLASS__
337 );
338 }
339
340 $result = [];
341 foreach ($rows as $row) {
342 if (array_key_exists('parentId', $row)) {
343 $row['parentId'] = $row['parentId'] !== null ? (int)$row['parentId'] : null;
344 }
345 $field = call_user_func([static::FACTORY, 'create'], $row);
346 $result[$field->getId()] = $field;
347 }
348
349 return $result;
350 }
351
352 /**
353 * Delete multiple fields by their IDs in bulk, and their field options.
354 *
355 * @param array<int> $ids
356 * @return int Number of deleted fields
357 * @throws QueryExecutionException
358 */
359 public function deleteMany(array $ids): int
360 {
361 if (empty($ids)) {
362 return 0;
363 }
364 $placeholders = implode(',', array_fill(0, count($ids), '%d'));
365 $deleteFieldsQuery = "DELETE FROM {$this->table} WHERE id IN ($placeholders)";
366 $result = $this->wpdb->query($this->wpdb->prepare($deleteFieldsQuery, ...$ids));
367 if ($result === false) {
368 throw new QueryExecutionException(
369 sprintf(
370 BackendStrings::getExceptionStrings()['failed_delete_by_ids'],
371 implode(', ', $ids),
372 $this->table
373 )
374 );
375 }
376 return $result;
377 }
378
379 /**
380 * Delete multiple fields by foreign key values (formId), and their field options.
381 *
382 * @param array<int> $formIds
383 * @param string $foreignColumn
384 * @return int Number of deleted fields
385 * @throws QueryExecutionException
386 */
387 public function deleteManyByForeignKeyValues(array $formIds, string $foreignColumn = 'formId'): int
388 {
389 if (empty($formIds)) {
390 return 0;
391 }
392 $formIds = array_map('intval', $formIds);
393 $placeholders = implode(',', array_fill(0, count($formIds), '%d'));
394 $deleteFieldsQuery = "DELETE FROM {$this->table} WHERE {$foreignColumn} IN ($placeholders)";
395 $result = $this->wpdb->query($this->wpdb->prepare($deleteFieldsQuery, ...$formIds));
396 if ($result === false) {
397 throw new QueryExecutionException(
398 BackendStrings::getExceptionStrings()['failed_delete_by_foreign_keys']
399 );
400 }
401 return $result;
402 }
403
404 /**
405 * Delete fields by a single foreign key value (formId), and their field options.
406 *
407 * @param int $formId
408 * @param string $foreignColumn
409 * @return int Number of deleted fields
410 * @throws QueryExecutionException
411 */
412 public function deleteOneByForeignKeyValue(int $formId, string $foreignColumn = 'formId'): int
413 {
414 $deleteFieldsQuery = "DELETE FROM {$this->table} WHERE {$foreignColumn} = %d";
415 $result = $this->wpdb->query($this->wpdb->prepare($deleteFieldsQuery, $formId));
416 if ($result === false) {
417 throw new QueryExecutionException(
418 BackendStrings::getExceptionStrings()['failed_delete_by_foreign_key']
419 );
420 }
421 return $result;
422 }
423 }
424