PluginProbe
The Innovative Form Builder – IvyForms / 0.8.2
The Innovative Form Builder – IvyForms v0.8.2
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.8.2, at backend/src/Services/Field/FieldService.php

385 lines 13.6 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\FieldOptionsRepositoryInterface;
18 use IvyForms\Repository\EntryField\EntryFieldRepositoryInterface;
19 use IvyForms\Services\Translations\BackendStrings;
20 use IvyForms\Common\Helpers\FieldHelper;
21
22 /**
23 * @SuppressWarnings("TooManyPublicMethods")
24 */
25 class FieldService
26 {
27 private FieldRepositoryInterface $fieldRepository;
28 private FieldOptionsRepositoryInterface $fieldOptionsRepository;
29 private EntryFieldRepositoryInterface $entryFieldRepository;
30
31 // Constructor injection for repositories only via PHP-DI
32 public function __construct(
33 FieldRepositoryInterface $fieldRepository,
34 FieldOptionsRepositoryInterface $fieldOptionsRepository,
35 EntryFieldRepositoryInterface $entryFieldRepository
36 ) {
37 $this->fieldRepository = $fieldRepository;
38 $this->fieldOptionsRepository = $fieldOptionsRepository;
39 $this->entryFieldRepository = $entryFieldRepository;
40 }
41
42 /**
43 * Create a new field.
44 *
45 * @param Field $fieldData
46 * @return int Field ID
47 * @throws InvalidArgumentException If the field data is invalid
48 */
49 public function addField(Field $fieldData): int
50 {
51 if (empty($fieldData->getFormId())) {
52 throw new InvalidArgumentException(
53 BackendStrings::getExceptionStrings()['form_id_required']
54 );
55 }
56 return $this->fieldRepository->add($fieldData);
57 }
58
59 /**
60 * Update an existing field.
61 *
62 * @param int $fieldId
63 * @param Field $fieldData
64 * @return bool
65 * @throws InvalidArgumentException If the field data is invalid
66 */
67 public function updateField(int $fieldId, Field $fieldData): bool
68 {
69 if (empty($fieldData->getId())) {
70 throw new InvalidArgumentException(
71 BackendStrings::getExceptionStrings()['field_id_required']
72 );
73 }
74 return $this->fieldRepository->update($fieldId, $fieldData);
75 }
76
77 /**
78 * Delete a field by its ID (removes options first).
79 *
80 * @param int $fieldId
81 * @return int
82 * @throws QueryExecutionException
83 */
84 public function deleteField(int $fieldId): int
85 {
86 $this->fieldOptionsRepository->deleteByFieldIds([$fieldId]);
87 return $this->fieldRepository->delete($fieldId);
88 }
89
90 /**
91 * Batch delete fields by IDs.
92 *
93 * @param int[] $fieldIds
94 * @return int Number of deleted rows
95 * @throws QueryExecutionException
96 */
97 public function deleteFields(array $fieldIds): int
98 {
99 $ids = array_values(array_filter(array_map('intval', $fieldIds), static fn($val) => $val > 0));
100 if (empty($ids)) {
101 return 0;
102 }
103 $this->fieldOptionsRepository->deleteByFieldIds($ids);
104 return $this->fieldRepository->deleteMany($ids);
105 }
106
107 /**
108 * Get all fields by form ID.
109 * @param int $id
110 * @return Field[]|null
111 */
112 public function getAllFields(int $id): ?array
113 {
114 return $this->fieldRepository->getFieldsById($id);
115 }
116
117 /**
118 * Get multiple fields by their IDs in a single query.
119 * @param int[] $fieldIds
120 * @return array<int, object> Indexed by field ID
121 */
122 public function getFieldsByIds(array $fieldIds): array
123 {
124 if (empty($fieldIds)) {
125 return [];
126 }
127 return $this->fieldRepository->getFieldsByIds($fieldIds);
128 }
129
130 /**
131 * Delete all fields for multiple form IDs.
132 * @param int[] $formIds
133 * @return int
134 * @throws InvalidArgumentException|QueryExecutionException
135 */
136 public function deleteFieldsByFormIds(array $formIds): int
137 {
138 if (empty($formIds)) {
139 throw new InvalidArgumentException(
140 BackendStrings::getExceptionStrings()['no_form_ids_provided']
141 );
142 }
143 $this->fieldOptionsRepository->deleteByFieldIdsByColumnValues('formId', $formIds);
144 return $this->fieldRepository->deleteManyByForeignKeyValues($formIds, 'formId');
145 }
146
147 /**
148 * Delete fields by single form ID.
149 * @param int $formId
150 * @return int
151 * @throws QueryExecutionException
152 */
153 public function deleteFieldsByFormId(int $formId): int
154 {
155 $this->fieldOptionsRepository->deleteByFieldIdsByColumnValues('formId', [$formId]);
156 return $this->fieldRepository->deleteOneByForeignKeyValue($formId, 'formId');
157 }
158
159 /**
160 * Duplicate all fields from one form to another.
161 * @param int $originalFormId
162 * @param int $newFormId
163 * @return void
164 * @throws InvalidArgumentException|ValidationException
165 */
166 public function duplicateFields(int $originalFormId, int $newFormId): void
167 {
168 $fields = $this->getAllFields($originalFormId);
169 $fieldIdMap = [];
170 foreach ($fields as $field) {
171 $fieldData = $field->toArray();
172 $oldId = $fieldData['id'];
173 unset($fieldData['id']);
174 $fieldData['formId'] = $newFormId;
175 FieldHelper::remapParentIdForDuplication($fieldData, $fieldIdMap);
176 $newFieldId = $this->addField(FieldFactory::create($fieldData));
177 $fieldIdMap[$oldId] = $newFieldId;
178 // Duplicate field options using helper
179 if (FieldHelper::fieldTypeHasOptions($field->getType())) {
180 FieldHelper::duplicateFieldOptions($oldId, $newFieldId, $this->fieldOptionsRepository);
181 }
182 }
183 }
184
185 /**
186 * Save fields with their options (radio/checkbox/select) for a form.
187 * @param array<int, array<string,mixed>> $fields
188 * @param int $formId
189 * @return void
190 * @throws ValidationException|InvalidArgumentException
191 */
192 public function saveFieldsWithOptions(array $fields, int $formId): void
193 {
194 $parentMap = [];
195 foreach ($fields as $fieldData) {
196 $fieldData['formId'] = $fieldData['formId'] ?? $formId;
197 // Resolve parentId using fieldIndex if available
198 FieldHelper::resolveParentId($fieldData, $parentMap);
199 $field = FieldFactory::create($fieldData);
200 $fieldId = $this->addField($field);
201 // If this is a parent field (no parentId), store its DB id in the map
202 if ($field->getParentId() === null) {
203 $parentMap[$field->getIndex()] = $fieldId;
204 }
205 if (FieldHelper::fieldTypeHasOptions($field->getType()) && !empty($fieldData['fieldOptions'])) {
206 foreach ($fieldData['fieldOptions'] as $optionData) {
207 // Always overwrite fieldId with the actual DB id of the field
208 $optionData['fieldId'] = $fieldId;
209 $option = FieldOptionsFactory::create($optionData);
210 $this->fieldOptionsRepository->add($option);
211 }
212 }
213 }
214 }
215
216 /**
217 * Update (and create/delete) fields and their options for a form.
218 * @param array<int, array<string,mixed>> $fields
219 * @param int $formId
220 * @return int[] Submitted field IDs
221 * @throws ValidationException|InvalidArgumentException
222 */
223 public function updateFieldsWithOptions(array $fields, int $formId): array
224 {
225 $submittedFieldIds = [];
226 $parentMap = [];
227 foreach ($fields as $fieldData) {
228 $fieldData['formId'] = $fieldData['formId'] ?? $formId;
229 FieldHelper::resolveParentId($fieldData, $parentMap);
230 $field = FieldFactory::create($fieldData);
231 $fieldId = $field->getId();
232
233 if ($fieldId === 0) { // new field
234 $newFieldId = $this->addField($field);
235 if ($field->getParentId() === null) {
236 $parentMap[$field->getIndex()] = $newFieldId;
237 }
238 $submittedFieldIds[] = $newFieldId;
239 if (empty($fieldData['fieldOptions'])) {
240 continue;
241 }
242 $newOptions = [];
243 foreach ($fieldData['fieldOptions'] as $optionData) {
244 $optionData['fieldId'] = $newFieldId;
245 $newOptions[] = FieldOptionsFactory::create($optionData);
246 }
247 $this->fieldOptionsRepository->addMany($newOptions);
248 continue;
249 }
250
251 // existing field
252 $this->updateField($fieldId, $field);
253 $submittedFieldIds[] = $fieldId;
254 if (empty($fieldData['fieldOptions'])) {
255 continue;
256 }
257 foreach ($fieldData['fieldOptions'] as &$optionData) {
258 $optionData['fieldId'] = $fieldId;
259 }
260 unset($optionData);
261 $this->batchProcessFieldOptions($fieldId, $fieldData);
262 }
263 return $submittedFieldIds;
264 }
265
266 /**
267 * Fetch all fields (with options for radio/checkbox/select types).
268 * @param int $formId
269 * @return array<int, array<string,mixed>>
270 * @throws ValidationException|QueryExecutionException
271 */
272 public function getAllFieldsWithOptions(int $formId): array
273 {
274 $fields = $this->fieldRepository->getFieldsById($formId);
275 $fieldsWithOptions = [];
276 foreach ($fields as $field) {
277 $fieldArr = $field->toArray();
278 if (FieldHelper::fieldTypeHasOptions($field->getType())) {
279 $options = $this->fieldOptionsRepository->getByFieldId($field->getId());
280 $fieldArr['fieldOptions'] = array_map(static fn($option) => $option->toArray(), $options);
281 }
282 $fieldsWithOptions[] = $fieldArr;
283 }
284 return $fieldsWithOptions;
285 }
286
287 /**
288 * Collect existing field IDs for a form.
289 * @param int $formId
290 * @return int[]
291 */
292 public function collectExistingFieldIds(int $formId): array
293 {
294 $existingFields = $this->getAllFields($formId);
295 return array_map(static fn($field) => $field->getId(), $existingFields ?? []);
296 }
297
298 /**
299 * Batch process field options: add, update, and delete as needed.
300 * @param int $fieldId
301 * @param array<string, mixed> $fieldData
302 * @throws ValidationException
303 */
304 public function batchProcessFieldOptions(
305 int $fieldId,
306 array $fieldData
307 ): void {
308 $existingOptions = $this->fieldOptionsRepository->getByFieldId($fieldId);
309 $existingOptionIds = array_map(static fn($opt) => $opt->getId(), $existingOptions);
310 $partition = FieldHelper::partitionFieldOptions($fieldData, $existingOptionIds);
311 // Add new options
312 $newOptionIds = $this->fieldOptionsRepository->addMany($partition['newOptions']);
313 // Update existing options
314 $this->fieldOptionsRepository->updateMany($partition['updateOptions']);
315 // Determine submitted option IDs (existing updated + newly created)
316 $submittedOptionIds = array_merge($partition['submittedOptionIds'], $newOptionIds);
317 // Delete orphaned (those that existed but were not resubmitted)
318 $orphanedOptionIds = array_diff($existingOptionIds, $submittedOptionIds);
319 if (!empty($orphanedOptionIds)) {
320 $this->fieldOptionsRepository->deleteByOptionIds($orphanedOptionIds);
321 }
322 }
323
324 /**
325 * Check for duplicate field values in submission.
326 * @param Field[] $formFields
327 * @param array<string,mixed> $submissionData
328 * @param int $formId
329 * @return array{0: bool, 1: array<int|string,bool>}
330 */
331 public function checkDuplicateFieldValues(
332 array $formFields,
333 array $submissionData,
334 int $formId
335 ): array {
336 $duplicateErrors = [];
337 $isDuplicate = false;
338 $fieldValues = [];
339
340 foreach ($formFields as $field) {
341 $fid = $field->getId();
342 $fidStr = (string)$fid;
343 if (isset($submissionData[$fidStr])) {
344 $fieldValues[$fidStr] = $submissionData[$fidStr];
345 }
346 if ($field->getFieldAdvancedSettings()->isNoDuplicates() && !empty($fieldValues[$fidStr])) {
347 $val = $fieldValues[$fidStr];
348 if ($this->entryFieldRepository->checkDuplicateValue($formId, $fid, $val)) {
349 $duplicateErrors[$fidStr] = true;
350 $isDuplicate = true;
351 }
352 }
353 }
354
355 return [$isDuplicate, $duplicateErrors];
356 }
357
358 /**
359 * Validate that all fields have allowed types.
360 * @param Field[]|mixed[] $formFields
361 * @throws InvalidArgumentException
362 */
363 public function validateFieldsType(array $formFields): void
364 {
365 // Validate field types
366 $allowedTypes = FieldType::getAllowedTypes();
367 foreach ($formFields as $field) {
368 // Check if field is an instance of Field or array
369 $fieldType = $field instanceof Field ? $field->getType() : ($field['type'] ?? null);
370
371 if ($fieldType === null) {
372 throw new InvalidArgumentException(
373 BackendStrings::getExceptionStrings()['invalid_field_type'] . ' (type not found)'
374 );
375 }
376
377 if (!in_array($fieldType, $allowedTypes, true)) {
378 throw new InvalidArgumentException(
379 BackendStrings::getExceptionStrings()['invalid_field_type'] . ' ' . $fieldType
380 );
381 }
382 }
383 }
384 }
385