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

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