PluginProbe
The Innovative Form Builder – IvyForms / 0.4
The Innovative Form Builder – IvyForms v0.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 / Services / Field / FieldService.php

FieldService.php in The Innovative Form Builder – IvyForms 0.4, at backend/src/Services/Field/FieldService.php

342 lines 11.3 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2
3 namespace IvyForms\Services\Field;
4
5 // phpcs:disable PSR1.Files.SideEffects
6 if (!defined('ABSPATH')) {
7 exit; // Exit if accessed directly
8 }
9
10 use IvyForms\Common\Exceptions\QueryExecutionException;
11 use IvyForms\Common\Exceptions\ValidationException;
12 use IvyForms\Entity\Field\Field;
13 use IvyForms\Common\Exceptions\InvalidArgumentException;
14 use IvyForms\Factory\Field\FieldFactory;
15 use IvyForms\Factory\FieldOptions\FieldOptionsFactory;
16 use IvyForms\Repository\Field\FieldRepositoryInterface;
17 use IvyForms\Repository\FieldOptions\FieldOptionsRepository;
18 use IvyForms\Services\FieldOptions\FieldOptionsService;
19 use IvyForms\Services\Translations\BackendStrings;
20 use IvyForms\Common\Helpers\Helper;
21 use IvyForms\Services\Entry\EntryService;
22
23 /**
24 * @SuppressWarnings("TooManyPublicMethods")
25 */
26 class FieldService
27 {
28 private FieldRepositoryInterface $fieldRepository;
29 private FieldOptionsRepository $fieldOptionsRepository;
30
31 // Constructor injection for the FieldRepository and FieldOptionsRepository via PHP-DI
32 public function __construct(
33 FieldRepositoryInterface $fieldRepository,
34 FieldOptionsRepository $fieldOptionsRepository
35 ) {
36 $this->fieldRepository = $fieldRepository;
37 $this->fieldOptionsRepository = $fieldOptionsRepository;
38 }
39
40 /**
41 * Create a new field.
42 *
43 * @param Field $fieldData
44 *
45 * @return int Form ID
46 *
47 * @throws InvalidArgumentException If the field data is invalid
48 */
49 public function addField(Field $fieldData): int
50 {
51 // Validate form data
52 if (empty($fieldData->getFormId())) {
53 throw new InvalidArgumentException(
54 BackendStrings::getExceptionStrings()['form_id_required']
55 );
56 }
57
58 // Create form via repository and return new form ID
59 return $this->fieldRepository->add($fieldData);
60 }
61
62
63 /**
64 * Update an existing field.
65 *
66 * @param int $fieldId
67 * @param Field $fieldData
68 *
69 * @return bool
70 *
71 * @throws InvalidArgumentException If the form data is invalid
72 */
73 public function updateField(int $fieldId, Field $fieldData): bool
74 {
75 // Validate form data before updating
76 if (empty($fieldData->getId())) {
77 throw new InvalidArgumentException(
78 BackendStrings::getExceptionStrings()['field_id_required']
79 );
80 }
81
82 return $this->fieldRepository->update($fieldId, $fieldData);
83 }
84
85 /**
86 * Delete a field by its ID.
87 *
88 * @param int $fieldId
89 * @return int
90 * @throws QueryExecutionException
91 */
92 public function deleteField(int $fieldId): int
93 {
94 // Delete field options first
95 $this->fieldOptionsRepository->deleteByFieldIds([$fieldId]);
96 // Then delete field
97 return $this->fieldRepository->delete($fieldId);
98 }
99
100 /**
101 * Batch delete fields by IDs using one SQL query.
102 *
103 * @param int[] $fieldIds Raw field IDs to attempt to delete (will be sanitized).
104 * @return int Number of deleted rows
105 * @throws QueryExecutionException
106 */
107 public function deleteFields(array $fieldIds): int
108 {
109 $ids = array_values(array_filter(array_map('intval', $fieldIds), static fn($value) => $value > 0));
110 if (empty($ids)) {
111 return 0;
112 }
113 // Delete field options first
114 $this->fieldOptionsRepository->deleteByFieldIds($ids);
115 // Then delete fields
116 return $this->fieldRepository->deleteMany($ids);
117 }
118
119 /**
120 * Get fields by form ID
121 *
122 * @param int $id
123 *
124 * @return mixed[]|null
125 */
126 public function getAllFields(int $id): ?array
127 {
128 return $this->fieldRepository->getFieldsById($id);
129 }
130
131
132 /**
133 * Delete fields by multiple form IDs.
134 *
135 * @param array<int> $formIds
136 * @return int Number of deleted fields
137 * @throws InvalidArgumentException If no form IDs are provided
138 * @throws QueryExecutionException
139 */
140 public function deleteFieldsByFormIds(array $formIds): int
141 {
142 if (empty($formIds)) {
143 throw new InvalidArgumentException(
144 BackendStrings::getExceptionStrings()['no_form_ids_provided']
145 );
146 }
147 // Delete field options first
148 $this->fieldOptionsRepository->deleteByFieldIdsByColumnValues('formId', $formIds);
149 // Then delete fields
150 return $this->fieldRepository->deleteManyByForeignKeyValues($formIds, 'formId');
151 }
152
153 /**
154 * Delete fields by a single form ID.
155 *
156 * @param int $formId
157 * @return int Number of deleted fields
158 * @throws QueryExecutionException
159 */
160 public function deleteFieldsByFormId(int $formId): int
161 {
162 // Secure deletion: only allow whitelisted column
163 $this->fieldOptionsRepository->deleteByFieldIdsByColumnValues('formId', [$formId]);
164 // Then delete fields
165 return $this->fieldRepository->deleteOneByForeignKeyValue($formId, 'formId');
166 }
167
168 /**
169 * @param int $originalFormId
170 * @param int $newFormId
171 *
172 * @return void
173 * @throws InvalidArgumentException|ValidationException
174 */
175 public function duplicateFields(int $originalFormId, int $newFormId): void
176 {
177 $fields = $this->getAllFields($originalFormId);
178 foreach ($fields as $field) {
179 $fieldData = $field->toArray();
180 unset($fieldData['id']);
181 $fieldData['formId'] = $newFormId;
182
183 $this->addField(FieldFactory::create($fieldData));
184 }
185 }
186
187 /**
188 * Save fields and their options for a form.
189 *
190 * @param array<mixed, mixed> $fields
191 * @param int $formId
192 * @param FieldOptionsService $fieldOptionsService
193 * @return void
194 * @throws ValidationException|InvalidArgumentException
195 */
196 public function saveFieldsWithOptions(array $fields, int $formId, FieldOptionsService $fieldOptionsService): void
197 {
198 foreach ($fields as $fieldData) {
199 $fieldData['formId'] = $fieldData['formId'] ?? $formId;
200 $field = FieldFactory::create($fieldData);
201 $fieldId = $this->addField($field);
202 // Always use fieldOptions for radio, checkbox, select
203 if (
204 in_array($field->getType(), ['radio', 'checkbox', 'select'], true)
205 && !empty($fieldData['fieldOptions'])
206 ) {
207 foreach ($fieldData['fieldOptions'] as $optionData) {
208 // Always overwrite fieldId with the actual DB id of the field
209 $optionData['fieldId'] = $fieldId;
210 $option = FieldOptionsFactory::create($optionData);
211 $fieldOptionsService->addOption($option);
212 }
213 }
214 }
215 }
216
217 /**
218 * Update fields and their options for a form.
219 *
220 * @param array<int, array<string, mixed>> $fields
221 * @param int $formId
222 * @param FieldOptionsService $fieldOptionsService
223 * @return array<int> List of submitted field IDs
224 * @throws ValidationException|InvalidArgumentException
225 */
226 public function updateFieldsWithOptions(array $fields, int $formId, FieldOptionsService $fieldOptionsService): array
227 {
228 $submittedFieldIds = [];
229 $parentMap = [];
230 foreach ($fields as $fieldData) {
231 $fieldData['formId'] = $fieldData['formId'] ?? $formId;
232 Helper::resolveParentId($fieldData, $parentMap);
233 $field = FieldFactory::create($fieldData);
234 $fieldId = $field->getId();
235 if ($fieldId === 0) {
236 // Add new field
237 $newFieldId = $this->addField($field);
238 if ($field->getParentId() === null) {
239 $parentMap[$field->getIndex()] = $newFieldId;
240 }
241 $submittedFieldIds[] = $newFieldId;
242 if (empty($fieldData['fieldOptions'])) {
243 continue;
244 }
245 $newOptions = [];
246 foreach ($fieldData['fieldOptions'] as $optionData) {
247 $optionData['fieldId'] = $newFieldId;
248 $newOptions[] = FieldOptionsFactory::create($optionData);
249 }
250 $fieldOptionsService->addOptions($newOptions);
251 continue;
252 }
253 $this->updateField($fieldId, $field);
254 $submittedFieldIds[] = $fieldId;
255 if (empty($fieldData['fieldOptions'])) {
256 continue;
257 }
258 foreach ($fieldData['fieldOptions'] as &$optionData) {
259 $optionData['fieldId'] = $fieldId;
260 }
261 unset($optionData);
262 Helper::batchProcessFieldOptions($fieldId, $fieldData, $fieldOptionsService, $this->fieldOptionsRepository);
263 }
264 return $submittedFieldIds;
265 }
266
267 /**
268 * Fetch all fields for a form, attaching fieldOptions for radio/checkbox/select fields.
269 *
270 * @param int $formId
271 * @param FieldOptionsRepository $fieldOptionsRepository
272 * @return array<array<string, mixed>>
273 * @throws ValidationException|QueryExecutionException
274 */
275 public function getAllFieldsWithOptions(int $formId, FieldOptionsRepository $fieldOptionsRepository): array
276 {
277 $fields = $this->fieldRepository->getFieldsById($formId);
278 $fieldsWithOptions = [];
279 foreach ($fields as $field) {
280 $fieldArr = $field->toArray();
281 // Attach fieldOptions for radio, checkbox, select
282 if (in_array($field->getType(), ['radio', 'checkbox', 'select'])) {
283 $options = $fieldOptionsRepository->getByFieldId($field->getId());
284 $fieldArr['fieldOptions'] = array_map(function ($option) {
285 return $option->toArray();
286 }, $options);
287 }
288 $fieldsWithOptions[] = $fieldArr;
289 }
290 return $fieldsWithOptions;
291 }
292
293 /**
294 * Collect existing field IDs for a form
295 * @return int[]
296 */
297 public function collectExistingFieldIds(int $formId): array
298 {
299 $existingFields = $this->getAllFields($formId);
300 return array_map(static fn($field) => $field->getId(), $existingFields ?? []);
301 }
302
303 /**
304 * Check for duplicate field values in a form submission.
305 *
306 * @param Field[] $formFields
307 * @param array<string, mixed> $submissionData
308 * @param int $formId
309 * @param EntryService $entryService
310 * @return array{0: bool, 1: array<int|string, bool>}
311 */
312 public function checkDuplicateFieldValues(
313 array $formFields,
314 array $submissionData,
315 int $formId,
316 EntryService $entryService
317 ): array {
318 $duplicateErrors = [];
319 $isDuplicate = false;
320 $fieldValues = [];
321
322 foreach ($formFields as $field) {
323 $fid = $field->getId();
324 $fidStr = (string)$fid;
325
326 if (isset($submissionData[$fidStr])) {
327 $fieldValues[$fidStr] = $submissionData[$fidStr];
328 }
329
330 if ($field->getFieldAdvancedSettings()->isNoDuplicates() && !empty($fieldValues[$fidStr])) {
331 $val = $fieldValues[$fidStr];
332 if ($entryService->checkDuplicateValue($formId, $fid, $val)) {
333 $duplicateErrors[$fidStr] = true;
334 $isDuplicate = true;
335 }
336 }
337 }
338
339 return [$isDuplicate, $duplicateErrors];
340 }
341 }
342