Refactor form submission processing with new FormSubmissionProcessor

- Introduce FormSubmissionProcessor service to handle synchronous/asynchronous form submission logic
- Modify PublicFormController to use new processor for submission and redirect handling
- Update StoreFormSubmissionJob to support processed data retrieval
- Add comprehensive test suite for FormSubmissionProcessor
- Improve handling of generated fields and redirect URL processing
This commit is contained in:
Julien Nahum 2025-02-01 22:52:06 +01:00
parent bb34cd98e5
commit f350ed778c
4 changed files with 254 additions and 30 deletions

View File

@ -9,7 +9,7 @@ use App\Http\Resources\FormSubmissionResource;
use App\Jobs\Form\StoreFormSubmissionJob;
use App\Models\Forms\Form;
use App\Models\Forms\FormSubmission;
use App\Open\MentionParser;
use App\Service\Forms\FormSubmissionProcessor;
use App\Service\Forms\FormCleaner;
use App\Service\WorkspaceHelper;
use Illuminate\Http\Request;
@ -85,7 +85,7 @@ class PublicFormController extends Controller
return redirect()->to($internal_url);
}
public function answer(AnswerFormRequest $request)
public function answer(AnswerFormRequest $request, FormSubmissionProcessor $formSubmissionProcessor)
{
$form = $request->form;
$isFirstSubmission = ($form->submissions_count === 0);
@ -95,10 +95,13 @@ class PublicFormController extends Controller
$completionTime = $request->get('completion_time') ?? null;
unset($submissionData['completion_time']); // Remove completion_time from the main data array
if ($form->editable_submissions) {
$job = new StoreFormSubmissionJob($form, $submissionData, $completionTime);
$job = new StoreFormSubmissionJob($form, $submissionData, $completionTime);
if ($formSubmissionProcessor->shouldProcessSynchronously($form)) {
$job->handle();
$submissionId = Hashids::encode($job->getSubmissionId());
// Update submission data with generated values for redirect URL
$submissionData = $job->getProcessedData();
} else {
StoreFormSubmissionJob::dispatch($form, $submissionData, $completionTime);
}
@ -107,27 +110,7 @@ class PublicFormController extends Controller
'message' => 'Form submission saved.',
'submission_id' => $submissionId,
'is_first_submission' => $isFirstSubmission,
], $this->getRedirectData($request->form, $submissionData)));
}
private function getRedirectData($form, $submissionData)
{
$formattedData = collect($submissionData)->map(function ($value, $key) {
return ['id' => $key, 'value' => $value];
})->values()->all();
$redirectUrl = ($form->redirect_url) ? (new MentionParser($form->redirect_url, $formattedData))->urlFriendlyOutput()->parseAsText() : null;
if ($redirectUrl && !filter_var($redirectUrl, FILTER_VALIDATE_URL)) {
$redirectUrl = null;
}
return $form->is_pro && $redirectUrl ? [
'redirect' => true,
'redirect_url' => $redirectUrl,
] : [
'redirect' => false,
];
], $formSubmissionProcessor->getRedirectData($form, $submissionData)));
}
public function fetchSubmission(Request $request, string $slug, string $submissionId)

View File

@ -27,6 +27,7 @@ class StoreFormSubmissionJob implements ShouldQueue
use SerializesModels;
public ?string $submissionId = null;
private ?array $formData = null;
/**
* Create a new job instance.
@ -44,13 +45,13 @@ class StoreFormSubmissionJob implements ShouldQueue
*/
public function handle()
{
$formData = $this->getFormData();
$this->addHiddenPrefills($formData);
$this->formData = $this->getFormData();
$this->addHiddenPrefills($this->formData);
$this->storeSubmission($formData);
$this->storeSubmission($this->formData);
$formData['submission_id'] = $this->submissionId;
FormSubmitted::dispatch($this->form, $formData);
$this->formData['submission_id'] = $this->submissionId;
FormSubmitted::dispatch($this->form, $this->formData);
}
public function getSubmissionId()
@ -258,4 +259,15 @@ class StoreFormSubmissionJob implements ShouldQueue
}
});
}
/**
* Get the processed form data after all transformations
*/
public function getProcessedData(): array
{
if ($this->formData === null) {
$this->formData = $this->getFormData();
}
return $this->formData;
}
}

View File

@ -0,0 +1,75 @@
<?php
namespace App\Service\Forms;
use App\Models\Forms\Form;
use App\Open\MentionParser;
class FormSubmissionProcessor
{
/**
* Determines if a form submission should be processed synchronously
*/
public function shouldProcessSynchronously(Form $form): bool
{
// If editable submissions is enabled, always process synchronously
if ($form->editable_submissions) {
return true;
}
// If no redirect URL, no need to process synchronously
if (!$form->redirect_url) {
return false;
}
// Check if any UUID/auto-increment fields are used in redirect URL
foreach ($form->properties as $field) {
if ($this->isGeneratedField($field) && $this->isFieldUsedInRedirectUrl($form, $field['id'])) {
return true;
}
}
return false;
}
/**
* Checks if a field is a generated field (UUID or auto-increment)
*/
private function isGeneratedField(array $field): bool
{
return $field['type'] === 'text' &&
(
(isset($field['generates_uuid']) && $field['generates_uuid']) ||
(isset($field['generates_auto_increment_id']) && $field['generates_auto_increment_id'])
);
}
/**
* Checks if a field ID is used in the form's redirect URL
*/
private function isFieldUsedInRedirectUrl(Form $form, string $fieldId): bool
{
return str_contains($form->redirect_url, '{' . $fieldId . '}');
}
/**
* Get the redirect data for a form submission
*/
public function getRedirectData(Form $form, array $submissionData): array
{
$redirectUrl = ($form->redirect_url)
? (new MentionParser($form->redirect_url, array_values($submissionData)))->urlFriendlyOutput()->parseAsText()
: null;
if ($redirectUrl && !filter_var($redirectUrl, FILTER_VALIDATE_URL)) {
$redirectUrl = null;
}
return $form->is_pro && $redirectUrl ? [
'redirect' => true,
'redirect_url' => $redirectUrl,
] : [
'redirect' => false,
];
}
}

View File

@ -0,0 +1,154 @@
<?php
use App\Service\Forms\FormSubmissionProcessor;
it('processes synchronously with editable submissions', function () {
$user = $this->actingAsUser();
$workspace = $this->createUserWorkspace($user);
$form = $this->createForm($user, $workspace, [
'editable_submissions' => true
]);
$processor = new FormSubmissionProcessor();
expect($processor->shouldProcessSynchronously($form))->toBeTrue();
});
it('processes synchronously with UUID field in redirect URL', function () {
$user = $this->actingAsUser();
$workspace = $this->createUserWorkspace($user);
$form = $this->createForm($user, $workspace, [
'redirect_url' => 'https://example.com/{field_1}',
'properties' => [
[
'id' => 'field_1',
'type' => 'text',
'generates_uuid' => true,
'name' => 'UUID Field'
]
]
]);
$processor = new FormSubmissionProcessor();
expect($processor->shouldProcessSynchronously($form))->toBeTrue();
});
it('processes synchronously with auto increment field in redirect URL', function () {
$user = $this->actingAsUser();
$workspace = $this->createUserWorkspace($user);
$form = $this->createForm($user, $workspace, [
'redirect_url' => 'https://example.com/{field_1}',
'properties' => [
[
'id' => 'field_1',
'type' => 'text',
'generates_auto_increment_id' => true,
'name' => 'Auto Increment Field'
]
]
]);
$processor = new FormSubmissionProcessor();
expect($processor->shouldProcessSynchronously($form))->toBeTrue();
});
it('processes asynchronously with no generated fields in redirect URL', function () {
$user = $this->actingAsUser();
$workspace = $this->createUserWorkspace($user);
$form = $this->createForm($user, $workspace, [
'redirect_url' => 'https://example.com/{field_1}',
'properties' => [
[
'id' => 'field_1',
'type' => 'text',
'name' => 'Regular Field'
]
]
]);
$processor = new FormSubmissionProcessor();
expect($processor->shouldProcessSynchronously($form))->toBeFalse();
});
it('processes asynchronously when generated field is not used in redirect URL', function () {
$user = $this->actingAsUser();
$workspace = $this->createUserWorkspace($user);
$form = $this->createForm($user, $workspace, [
'redirect_url' => 'https://example.com/{field_2}',
'properties' => [
[
'id' => 'field_1',
'type' => 'text',
'generates_uuid' => true,
'name' => 'UUID Field'
],
[
'id' => 'field_2',
'type' => 'text',
'name' => 'Regular Field'
]
]
]);
$processor = new FormSubmissionProcessor();
expect($processor->shouldProcessSynchronously($form))->toBeFalse();
});
it('processes asynchronously with no redirect URL', function () {
$user = $this->actingAsUser();
$workspace = $this->createUserWorkspace($user);
$form = $this->createForm($user, $workspace, [
'redirect_url' => null,
'properties' => [
[
'id' => 'field_1',
'type' => 'text',
'generates_uuid' => true,
'name' => 'UUID Field'
]
]
]);
$processor = new FormSubmissionProcessor();
expect($processor->shouldProcessSynchronously($form))->toBeFalse();
});
it('formats redirect data correctly for pro users', function () {
$user = $this->actingAsProUser();
$workspace = $this->createUserWorkspace($user);
$form = $this->createForm($user, $workspace, [
'redirect_url' => 'https://example.com/<span mention mention-field-id="field_1"></span>'
]);
$processor = new FormSubmissionProcessor();
$redirectData = $processor->getRedirectData($form, [
'field_1' => [
'id' => 'field_1',
'value' => 'test-value'
]
]);
expect($redirectData)->toBe([
'redirect' => true,
'redirect_url' => 'https://example.com/test-value'
]);
});
it('returns no redirect for non-pro users', function () {
$user = $this->actingAsUser();
$workspace = $this->createUserWorkspace($user);
$form = $this->createForm($user, $workspace, [
'redirect_url' => 'https://example.com/<span mention mention-field-id="field_1"></span>'
]);
$processor = new FormSubmissionProcessor();
$redirectData = $processor->getRedirectData($form, [
'field_1' => [
'id' => 'field_1',
'value' => 'test-value'
]
]);
expect($redirectData)->toBe([
'redirect' => false
]);
});