PluginProbe
The Innovative Form Builder – IvyForms / 1.5.1
The Innovative Form Builder – IvyForms v1.5.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
← All changes | backend/src/Services/Field/FieldService.php +246 -42 0.2 → 1.5.1 View file →
@@ -1,28 +1,38 @@
1 1 <?php
2 2
3 3 namespace IvyForms\Services\Field;
4 4
5 -// phpcs:disable PSR1.Files.SideEffects
6 -if (!defined('ABSPATH')) {
7 - exit; // Exit if accessed directly
8 -}
9 -
10 -use IvyForms\Common\Exceptions\NotFoundException;
5 +use IvyForms\Common\Exceptions\QueryExecutionException;
11 6 use IvyForms\Common\Exceptions\ValidationException;
12 7 use IvyForms\Entity\Field\Field;
13 8 use IvyForms\Common\Exceptions\InvalidArgumentException;
14 9 use IvyForms\Factory\Field\FieldFactory;
10 +use IvyForms\Factory\FieldOptions\FieldOptionsFactory;
15 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 16
17 +/**
18 + * @SuppressWarnings("TooManyPublicMethods")
19 + */
17 20 class FieldService
18 21 {
19 22 private FieldRepositoryInterface $fieldRepository;
23 + private FieldOptionsRepositoryInterface $fieldOptionsRepository;
24 + private ImageService $imageService;
20 25
21 - // Constructor injection for the FieldRepository via PHP-DI
22 - public function __construct(FieldRepositoryInterface $fieldRepository)
23 - {
26 + // Constructor injection for repositories only via PHP-DI
27 + public function __construct(
28 + FieldRepositoryInterface $fieldRepository,
29 + FieldOptionsRepositoryInterface $fieldOptionsRepository,
30 + ImageService $imageService
31 + ) {
24 32 $this->fieldRepository = $fieldRepository;
33 + $this->fieldOptionsRepository = $fieldOptionsRepository;
34 + $this->imageService = $imageService;
25 35 }
26 36
27 37 /**
28 38 * Create a new field.
@@ -27,62 +37,73 @@
27 37 /**
28 38 * Create a new field.
29 39 *
30 40 * @param Field $fieldData
31 - *
32 - * @return int Form ID
33 - *
41 + * @return int Field ID
34 42 * @throws InvalidArgumentException If the field data is invalid
35 43 */
36 44 public function addField(Field $fieldData): int
37 45 {
38 - // Validate form data
39 46 if (empty($fieldData->getFormId())) {
40 - throw new InvalidArgumentException('Form id is required.');
47 + throw new InvalidArgumentException(
48 + BackendStrings::getExceptionStrings()['form_id_required']
49 + );
41 50 }
42 -
43 - // Create form via repository and return new form ID
44 51 return $this->fieldRepository->add($fieldData);
45 52 }
46 53
47 -
48 54 /**
49 55 * Update an existing field.
50 56 *
51 57 * @param int $fieldId
52 58 * @param Field $fieldData
53 - *
54 59 * @return bool
55 - *
56 - * @throws InvalidArgumentException If the form data is invalid
60 + * @throws InvalidArgumentException If the field data is invalid
57 61 */
58 62 public function updateField(int $fieldId, Field $fieldData): bool
59 63 {
60 - // Validate form data before updating
61 64 if (empty($fieldData->getId())) {
62 - throw new InvalidArgumentException('Filed id is required.');
65 + throw new InvalidArgumentException(
66 + BackendStrings::getExceptionStrings()['field_id_required']
67 + );
63 68 }
64 -
65 69 return $this->fieldRepository->update($fieldId, $fieldData);
66 70 }
67 71
68 72 /**
69 - * Delete a field by its ID.
73 + * Delete a field by its ID (removes options first).
70 74 *
71 75 * @param int $fieldId
72 76 * @return int
77 + * @throws QueryExecutionException
73 78 */
74 79 public function deleteField(int $fieldId): int
75 80 {
81 + $this->fieldOptionsRepository->deleteByFieldIds([$fieldId]);
76 82 return $this->fieldRepository->delete($fieldId);
77 83 }
78 84
79 85 /**
80 - * Get fields by form ID
86 + * Batch delete fields by IDs.
81 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.
82 104 * @param int $id
83 - *
84 - * @return mixed[]|null
105 + * @return Field[]|null
85 106 */
86 107 public function getAllFields(int $id): ?array
87 108 {
88 109 return $this->fieldRepository->getFieldsById($id);
@@ -88,32 +109,53 @@
88 109 return $this->fieldRepository->getFieldsById($id);
89 110 }
90 111
91 112 /**
92 - * Delete fields by form ID
93 - *
94 - * @param int $id
95 - *
96 - * @throws NotFoundException
97 - * @throws InvalidArgumentException
113 + * Get multiple fields by their IDs in a single query.
114 + * @param int[] $fieldIds
115 + * @return array<int, object> Indexed by field ID
98 116 */
99 - public function deleteFieldsByFormId(int $id): void
117 + public function getFieldsByIds(array $fieldIds): array
100 118 {
101 - $fields = $this->getAllFields($id);
119 + if (empty($fieldIds)) {
120 + return [];
121 + }
122 + return $this->fieldRepository->getFieldsByIds($fieldIds);
123 + }
102 124
103 - if (!$fields) {
104 - return;
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 + );
105 137 }
138 + $this->fieldOptionsRepository->deleteByFieldIdsByColumnValues('formId', $formIds);
139 + return $this->fieldRepository->deleteManyByForeignKeyValues($formIds, 'formId');
140 + }
106 141
107 - foreach ($fields as $field) {
108 - $this->deleteField($field->getId());
109 - }
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');
110 152 }
111 153
112 154 /**
155 + * Duplicate all fields from one form to another.
113 156 * @param int $originalFormId
114 157 * @param int $newFormId
115 - *
116 158 * @return void
117 159 * @throws InvalidArgumentException|ValidationException
118 160 */
119 161 public function duplicateFields(int $originalFormId, int $newFormId): void
@@ -118,13 +160,175 @@
118 160 */
119 161 public function duplicateFields(int $originalFormId, int $newFormId): void
120 162 {
121 163 $fields = $this->getAllFields($originalFormId);
164 + $fieldIdMap = [];
122 165 foreach ($fields as $field) {
123 166 $fieldData = $field->toArray();
167 + $oldId = $fieldData['id'];
124 168 unset($fieldData['id']);
125 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 + }
126 179
127 - $this->addField(FieldFactory::create($fieldData));
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 + }
128 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);
129 333 }
130 334 }