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

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

350 lines 12.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 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 to upload base64 images
195 FieldHelper::processFieldHtmlContent($fieldData, $this->imageService);
196 $field = FieldFactory::create($fieldData);
197 $fieldId = $this->addField($field);
198 // If this is a parent field (no parentId), store its DB id in the map
199 if ($field->getParentId() === null) {
200 $parentMap[$field->getIndex()] = $fieldId;
201 }
202 if (FieldHelper::fieldTypeHasOptions($field->getType()) && !empty($fieldData['fieldOptions'])) {
203 foreach ($fieldData['fieldOptions'] as $optionData) {
204 // Always overwrite fieldId with the actual DB id of the field
205 $optionData['fieldId'] = $fieldId;
206 $option = FieldOptionsFactory::create($optionData);
207 $this->fieldOptionsRepository->add($option);
208 }
209 }
210 }
211 }
212
213 /**
214 * Update (and create/delete) fields and their options for a form.
215 * @param array<int, array<string,mixed>> $fields
216 * @param int $formId
217 * @return int[] Submitted field IDs
218 * @throws ValidationException|InvalidArgumentException
219 */
220 public function updateFieldsWithOptions(array $fields, int $formId): array
221 {
222 $submittedFieldIds = [];
223 $parentMap = [];
224 foreach ($fields as $fieldData) {
225 $fieldData['formId'] = $fieldData['formId'] ?? $formId;
226 FieldHelper::resolveParentId($fieldData, $parentMap);
227 // Process htmlContent to upload base64 images
228 FieldHelper::processFieldHtmlContent($fieldData, $this->imageService);
229 $field = FieldFactory::create($fieldData);
230 $fieldId = $field->getId();
231
232 if ($fieldId === 0) { // new field
233 $newFieldId = $this->addField($field);
234 if ($field->getParentId() === null) {
235 $parentMap[$field->getIndex()] = $newFieldId;
236 }
237 $submittedFieldIds[] = $newFieldId;
238 if (empty($fieldData['fieldOptions'])) {
239 continue;
240 }
241 $newOptions = [];
242 foreach ($fieldData['fieldOptions'] as $optionData) {
243 $optionData['fieldId'] = $newFieldId;
244 $newOptions[] = FieldOptionsFactory::create($optionData);
245 }
246 $this->fieldOptionsRepository->addMany($newOptions);
247 continue;
248 }
249
250 // existing field
251 $this->updateField($fieldId, $field);
252 $submittedFieldIds[] = $fieldId;
253 if (empty($fieldData['fieldOptions'])) {
254 continue;
255 }
256 foreach ($fieldData['fieldOptions'] as &$optionData) {
257 $optionData['fieldId'] = $fieldId;
258 }
259 unset($optionData);
260 $this->batchProcessFieldOptions($fieldId, $fieldData);
261 }
262 return $submittedFieldIds;
263 }
264
265 /**
266 * Fetch all fields (with options for radio/checkbox/select types).
267 * @param int $formId
268 * @return array<int, array<string,mixed>>
269 * @throws ValidationException|QueryExecutionException
270 */
271 public function getAllFieldsWithOptions(int $formId): array
272 {
273 $fields = $this->fieldRepository->getFieldsById($formId);
274 $fieldsWithOptions = [];
275 foreach ($fields as $field) {
276 $fieldArr = $field->toArray();
277 if (FieldHelper::fieldTypeHasOptions($field->getType())) {
278 $options = $this->fieldOptionsRepository->getByFieldId($field->getId());
279 $fieldArr['fieldOptions'] = array_map(static fn($option) => $option->toArray(), $options);
280 }
281 $fieldsWithOptions[] = $fieldArr;
282 }
283 return $fieldsWithOptions;
284 }
285
286 /**
287 * Collect existing field IDs for a form.
288 * @param int $formId
289 * @return int[]
290 */
291 public function collectExistingFieldIds(int $formId): array
292 {
293 $existingFields = $this->getAllFields($formId);
294 return array_map(static fn($field) => $field->getId(), $existingFields ?? []);
295 }
296
297 /**
298 * Batch process field options: add, update, and delete as needed.
299 * @param int $fieldId
300 * @param array<string, mixed> $fieldData
301 * @throws ValidationException
302 */
303 public function batchProcessFieldOptions(
304 int $fieldId,
305 array $fieldData
306 ): void {
307 $existingOptions = $this->fieldOptionsRepository->getByFieldId($fieldId);
308 $existingOptionIds = array_map(static fn($opt) => $opt->getId(), $existingOptions);
309 $partition = FieldHelper::partitionFieldOptions($fieldData, $existingOptionIds);
310 // Add new options
311 $newOptionIds = $this->fieldOptionsRepository->addMany($partition['newOptions']);
312 // Update existing options
313 $this->fieldOptionsRepository->updateMany($partition['updateOptions']);
314 // Determine submitted option IDs (existing updated + newly created)
315 $submittedOptionIds = array_merge($partition['submittedOptionIds'], $newOptionIds);
316 // Delete orphaned (those that existed but were not resubmitted)
317 $orphanedOptionIds = array_diff($existingOptionIds, $submittedOptionIds);
318 if (!empty($orphanedOptionIds)) {
319 $this->fieldOptionsRepository->deleteByOptionIds($orphanedOptionIds);
320 }
321 }
322
323 /**
324 * Validate that all fields have allowed types.
325 * @param Field[]|mixed[] $formFields
326 * @throws InvalidArgumentException
327 */
328 public function validateFieldsType(array $formFields): void
329 {
330 // Validate field types
331 $allowedTypes = FieldType::getAllowedTypes();
332 foreach ($formFields as $field) {
333 // Check if field is an instance of Field or array
334 $fieldType = $field instanceof Field ? $field->getType() : ($field['type'] ?? null);
335
336 if ($fieldType === null) {
337 throw new InvalidArgumentException(
338 BackendStrings::getExceptionStrings()['invalid_field_type'] . ' (type not found)'
339 );
340 }
341
342 if (!in_array($fieldType, $allowedTypes, true)) {
343 throw new InvalidArgumentException(
344 BackendStrings::getExceptionStrings()['invalid_field_type'] . ' ' . $fieldType
345 );
346 }
347 }
348 }
349 }
350