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:
parent
bb34cd98e5
commit
f350ed778c
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
];
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
]);
|
||||
});
|
||||
Loading…
Reference in New Issue