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 / Form / FormImportExportService.php

FormImportExportService.php in The Innovative Form Builder – IvyForms 1.5, at backend/src/Services/Form/FormImportExportService.php

389 lines 14.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\Form;
4
5 use IvyForms\Common\Helpers\FormImportValidator;
6 use IvyForms\Common\Helpers\Import\FormImportNormalizer;
7 use IvyForms\Common\Exceptions\InvalidArgumentException;
8 use IvyForms\Common\Exceptions\NotFoundException;
9 use IvyForms\Common\Exceptions\QueryExecutionException;
10 use IvyForms\Common\Exceptions\ValidationException;
11 use IvyForms\ValueObjects\Form\FormExportOptions;
12 use IvyForms\Services\Field\FieldService;
13 use IvyForms\Services\Notification\NotificationService;
14 use IvyForms\Services\Confirmation\ConfirmationService;
15 use IvyForms\Services\Permissions\AccessRulesFormScopeExtender;
16 use IvyForms\Services\Translations\BackendStrings;
17 use IvyForms\Factory\Form\FormFactory;
18 use IvyForms\Factory\Notification\NotificationFactory;
19 use IvyForms\Factory\Confirmation\ConfirmationFactory;
20
21 class FormImportExportService
22 {
23 private FormService $formService;
24 private FieldService $fieldService;
25 private NotificationService $notificationService;
26 private ConfirmationService $confirmationService;
27
28 private AccessRulesFormScopeExtender $accessRulesFormScopeExtender;
29
30 public function __construct(
31 FormService $formService,
32 FieldService $fieldService,
33 NotificationService $notificationService,
34 ConfirmationService $confirmationService,
35 AccessRulesFormScopeExtender $accessRulesFormScopeExtender
36 ) {
37 $this->formService = $formService;
38 $this->fieldService = $fieldService;
39 $this->notificationService = $notificationService;
40 $this->confirmationService = $confirmationService;
41 $this->accessRulesFormScopeExtender = $accessRulesFormScopeExtender;
42 }
43
44 /**
45 * Export multiple forms.
46 *
47 * @param array<int> $formIds
48 * @param FormExportOptions|null $exportOptions Export section options
49 * @return array<string, mixed>
50 */
51 public function exportForms(array $formIds, ?FormExportOptions $exportOptions = null): array
52 {
53 $exportOptions = $exportOptions ?? FormExportOptions::withStyles();
54
55 /**
56 * Allows extensions to adjust export options (e.g. include entries).
57 *
58 * @since 0.1.0
59 *
60 * @param FormExportOptions $exportOptions Export options from the request.
61 * @param array<int> $formIds Form IDs being exported.
62 * @return FormExportOptions Modified export options.
63 */
64 $exportOptions = apply_filters('ivyforms/form/import_export/export_options', $exportOptions, $formIds);
65 $exportedForms = [];
66
67 foreach ($formIds as $formId) {
68 $formData = $this->exportSingleForm($formId, $exportOptions);
69 if ($formData) {
70 $exportedForms[] = $formData;
71 }
72 }
73
74 $exportPayload = [
75 'version' => IVYFORMS_VERSION,
76 'export_date' => current_time('mysql'),
77 'forms' => $exportedForms,
78 'forms_count' => count($exportedForms)
79 ];
80
81 /**
82 * Allows extending the final form export payload.
83 * Useful for adding top-level metadata or future sections related to export/import.
84 *
85 * @since 0.1.0
86 *
87 * @param array<string, mixed> $exportPayload The complete export payload.
88 * @param array<int> $formIds The requested form IDs.
89 * @param array<int, array<string, mixed>> $exportedForms The exported forms payload.
90 * @return array<string, mixed> Modified export payload.
91 */
92 return apply_filters('ivyforms/form/import_export/export_payload', $exportPayload, $formIds, $exportedForms);
93 }
94
95 /**
96 * Export a single form with all its data.
97 *
98 * @param int $formId
99 * @param FormExportOptions $exportOptions Export section options
100 * @return array<string, mixed>
101 * @throws NotFoundException
102 * @throws ValidationException|QueryExecutionException
103 */
104 private function exportSingleForm(int $formId, FormExportOptions $exportOptions): array
105 {
106 $form = $this->formService->getFormById($formId);
107 $fields = $this->fieldService->getAllFieldsWithOptions($formId);
108 $idToIndexMap = FormImportValidator::buildFieldIdToIndexMap($fields);
109 $notifications = $this->notificationService->getNotificationsByFormId($formId);
110 $confirmations = $this->confirmationService->getConfirmationsById($formId);
111 $confirmation = !empty($confirmations) ? $confirmations[0] : null;
112
113 $formExportData = FormImportValidator::prepareFormDataForExport($form);
114 if (!$exportOptions->includesStyles()) {
115 $formExportData = FormImportValidator::stripStyleSettingsFromFormExport($formExportData);
116 }
117
118 $exportSections = [
119 'form' => $formExportData,
120 'fields' => FormImportValidator::prepareFieldsDataForExport($fields, $idToIndexMap),
121 'notifications' => FormImportValidator::prepareNotificationsDataForExport($notifications),
122 'confirmation' => FormImportValidator::prepareConfirmationDataForExport($confirmation),
123 ];
124
125 /**
126 * Allows extending exported sections for a single form.
127 * Useful for adding future sections like entries, styles, or other custom form data.
128 *
129 * @since 0.1.0
130 *
131 * @param array<string, mixed> $exportSections The current export sections for a single form.
132 * @param int $formId The source form ID.
133 * @param mixed $form The source form entity.
134 * @param array<int, array<string, mixed>> $fields The source form fields.
135 * @param array<int, mixed> $notifications The source notifications.
136 * @param mixed $confirmation The source confirmation entity.
137 * @param FormExportOptions $exportOptions Export options for this request.
138 * @return array<string, mixed> Modified export sections.
139 */
140 return apply_filters(
141 'ivyforms/form/import_export/export_sections',
142 $exportSections,
143 $formId,
144 $form,
145 $fields,
146 $notifications,
147 $confirmation,
148 $exportOptions
149 );
150 }
151
152 /**
153 * Normalize a raw import payload (IvyForms or third-party), validate it,
154 * and import the resulting forms.
155 *
156 * Shared entry point for REST, MCP, and any future API callers.
157 *
158 * @param mixed $importData
159 * @return array<int, array<string, mixed>>
160 * @throws InvalidArgumentException
161 */
162 public function importFromRawPayload($importData): array
163 {
164 $normalizedData = FormImportNormalizer::normalize($importData);
165 $formsData = FormImportValidator::validateImportPayload($normalizedData);
166 $this->assertHasMappedFields($formsData);
167
168 return $this->importForms($formsData);
169 }
170
171 /**
172 * Reject imports where every form has zero mapped fields.
173 *
174 * @param array<int, array<string, mixed>> $formsData
175 * @return void
176 * @throws InvalidArgumentException
177 */
178 private function assertHasMappedFields(array $formsData): void
179 {
180 foreach ($formsData as $formData) {
181 if (!empty($formData['fields']) && is_array($formData['fields'])) {
182 return;
183 }
184 }
185
186 throw new InvalidArgumentException(
187 BackendStrings::getExceptionStrings()['import_no_supported_fields']
188 );
189 }
190
191 /**
192 * Import multiple forms
193 * @param array<int, array<string, mixed>> $formsData
194 * @return array<int, array<string, mixed>>
195 */
196 public function importForms(array $formsData): array
197 {
198 /**
199 * Allows preprocessing imported forms data before per-form import starts.
200 * Useful for normalizing or enriching custom import sections.
201 *
202 * @since 0.1.0
203 *
204 * @param array<int, array<string, mixed>> $formsData The validated forms data to import.
205 * @return array<int, array<string, mixed>> Modified forms data.
206 */
207 $formsData = apply_filters('ivyforms/form/import_export/import_forms_data', $formsData);
208
209 $importedForms = [];
210 foreach ($formsData as $formData) {
211 $importedForm = $this->importSingleForm($formData);
212 if ($importedForm) {
213 $importedForms[] = $importedForm;
214 }
215 }
216 return $importedForms;
217 }
218
219 /**
220 * Import a single form with all its data
221 * @param array<string, mixed> $formData
222 * @return array<string, mixed>|null
223 */
224 private function importSingleForm(array $formData): ?array
225 {
226 /**
227 * Allows preprocessing a single imported form payload before core import runs.
228 * Useful for future custom sections such as entries or styles.
229 *
230 * @since 0.1.0
231 *
232 * @param array<string, mixed> $formData The single form payload being imported.
233 * @return array<string, mixed> Modified single form payload.
234 */
235 $formData = apply_filters('ivyforms/form/import_export/import_form_data', $formData);
236
237 $currentUser = wp_get_current_user();
238 $author = $currentUser->user_login ?? 'admin';
239 $formArray = FormImportValidator::prepareFormArrayForImport(
240 $formData,
241 $author,
242 current_time('mysql'),
243 BackendStrings::getCommonStrings()['submit']
244 );
245 $formId = $this->createFormForImport($formArray);
246 if (!$formId) {
247 return null;
248 }
249
250 $this->accessRulesFormScopeExtender->appendCreatedFormToCurrentUserScope($formId);
251
252 /**
253 * Fires after the core form record is created during import.
254 * Useful for importing form-related metadata that only needs the new form ID.
255 *
256 * @since 0.1.0
257 *
258 * @param int $formId The newly created form ID.
259 * @param array<string, mixed> $formData The imported single form payload.
260 * @param array<string, mixed> $formArray The prepared form array used to create the form.
261 */
262 do_action('ivyforms/form/import_export/after_form_created', $formId, $formData, $formArray);
263
264 $formName = $this->resolveImportedFormName($formArray);
265 $this->importFieldsForForm($formData, $formId);
266 $this->importNotificationsForForm($formData, $formId, $formName);
267 $this->importConfirmationForForm($formData, $formId);
268
269 /**
270 * Fires after the core import process finishes for a single form.
271 * Useful for importing dependent sections such as entries or styles.
272 *
273 * @since 0.1.0
274 *
275 * @param int $formId The newly created form ID.
276 * @param array<string, mixed> $formData The imported single form payload.
277 * @param array<string, mixed> $formArray The prepared form array used to create the form.
278 * @param string $formName The resolved imported form name.
279 */
280 do_action('ivyforms/form/import_export/after_import', $formId, $formData, $formArray, $formName);
281
282 return [
283
284 'id' => $formId,
285 'name' => $formName
286 ];
287 }
288
289 /**
290 * Create a form record for import.
291 * @param array<string, mixed> $formArray
292 * @return int|null
293 */
294 private function createFormForImport(array $formArray): ?int
295 {
296 $form = FormFactory::create($formArray);
297 $formId = $this->formService->createForm($form);
298 if (!$formId) {
299 return null;
300 }
301
302 return $formId;
303 }
304
305 /**
306 * Resolve final form name for import response and defaults.
307 * @param array<string, mixed> $formArray
308 * @return string
309 */
310 private function resolveImportedFormName(array $formArray): string
311 {
312 if (!empty($formArray['name'])) {
313 return (string)$formArray['name'];
314 }
315
316 return BackendStrings::getAllFormsStrings()['imported_form'];
317 }
318
319 /**
320 * Import form fields if present.
321 * @param array<string, mixed> $formData
322 * @param int $formId
323 * @return void
324 */
325 private function importFieldsForForm(array $formData, int $formId): void
326 {
327 $fieldsData = $formData['fields'] ?? [];
328 if (empty($fieldsData)) {
329 return;
330 }
331
332 $fieldsData = FormImportValidator::normalizeFieldsForImport($fieldsData);
333
334 $this->fieldService->saveFieldsWithOptions($fieldsData, $formId);
335 }
336
337 /**
338 * Import notifications or create defaults when missing.
339 * @param array<string, mixed> $formData
340 * @param int $formId
341 * @param string $formName
342 * @return void
343 */
344 private function importNotificationsForForm(array $formData, int $formId, string $formName): void
345 {
346 $notificationsData = $formData['notifications'] ?? [];
347 if (empty($notificationsData)) {
348 $adminEmail = get_option('admin_email');
349 $this->notificationService->createDefaultNotificationForForm(
350 $formId,
351 $formName,
352 is_string($adminEmail) ? $adminEmail : ''
353 );
354 return;
355 }
356
357 foreach ($notificationsData as $notificationData) {
358 $notificationData['id'] = 0;
359 $notificationData['formId'] = $formId;
360 $notification = NotificationFactory::create($notificationData);
361 $this->notificationService->createNotification($notification);
362 }
363 }
364
365 /**
366 * Import confirmation or create default one when missing.
367 * @param array<string, mixed> $formData
368 * @param int $formId
369 * @return void
370 */
371 private function importConfirmationForForm(array $formData, int $formId): void
372 {
373 $confirmationData = $formData['confirmation'] ?? null;
374 if (is_object($confirmationData)) {
375 $confirmationData = (array)$confirmationData;
376 }
377
378 if (!is_array($confirmationData) || empty($confirmationData)) {
379 $this->confirmationService->createDefaultConfirmationForForm($formId);
380 return;
381 }
382
383 $confirmationData['id'] = 0;
384 $confirmationData['formId'] = $formId;
385 $confirmation = ConfirmationFactory::create($confirmationData);
386 $this->confirmationService->createConfirmation($confirmation);
387 }
388 }
389