Refactor Form Submission Logic and Improve File Handling

- Updated `FormController.php` to correct the file path concatenation for temporary file retrieval, enhancing clarity in file management.
- Modified `FormSubmissionResource.php` to improve the filtering logic for file fields, ensuring only valid files are processed.
- Streamlined the constructor in `StoreFormSubmissionJob.php` for better readability and consistency.
- Enhanced file handling in `StoreFormSubmissionJob.php` by refining the logic for moving files from temporary to permanent storage, ensuring accurate file management.
- Improved the `FormSubmissionFormatter.php` to handle file URLs more robustly with error handling for signed routes.
- Updated `PartialSubmissionTest.php` to remove the hardcoded success message, improving test reliability.
- Refactored `OpenFormField.vue` to simplify the disabled state logic for form fields, enhancing maintainability.
- Adjusted `usePartialSubmission.js` to improve the synchronization mechanism during visibility changes, ensuring data integrity.

These changes aim to enhance the overall functionality, maintainability, and reliability of the form submission process and file handling logic.
This commit is contained in:
Julien Nahum
2025-05-07 20:03:47 +02:00
parent 053abbf31b
commit 2edd0816d4
8 changed files with 81 additions and 92 deletions

View File

@@ -17,7 +17,6 @@ use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Queue\SerializesModels;
use Illuminate\Support\Facades\Storage;
use Illuminate\Support\Str;
use Illuminate\Support\Facades\Log;
/**
* Job to store form submissions
@@ -68,20 +67,11 @@ class StoreFormSubmissionJob implements ShouldQueue
*/
public function handle()
{
// Extract metadata from submission data
$this->extractMetadata();
// Process form data
$this->formData = $this->getFormData();
$this->addHiddenPrefills($this->formData);
// Store the submission
$this->storeSubmission($this->formData);
// Add the submission ID to the form data after storing the submission
$this->formData['submission_id'] = $this->submissionId;
// Only trigger integrations for completed submissions, not partial ones
if (!$this->isPartial) {
FormSubmitted::dispatch($this->form, $this->formData);
}
@@ -97,21 +87,16 @@ class StoreFormSubmissionJob implements ShouldQueue
*/
private function extractMetadata(): void
{
// Extract completion time
if (isset($this->submissionData['completion_time'])) {
$this->completionTime = $this->submissionData['completion_time'];
unset($this->submissionData['completion_time']);
}
// Extract direct submission ID if present
if (isset($this->submissionData['submission_id']) && $this->submissionData['submission_id']) {
if (is_numeric($this->submissionData['submission_id'])) {
$this->submissionId = (int)$this->submissionData['submission_id'];
}
unset($this->submissionData['submission_id']);
}
// Extract is_partial flag if present, otherwise default to false
if (isset($this->submissionData['is_partial'])) {
$this->isPartial = (bool)$this->submissionData['is_partial'];
unset($this->submissionData['is_partial']);
@@ -135,27 +120,18 @@ class StoreFormSubmissionJob implements ShouldQueue
*/
private function storeSubmission(array $formData)
{
// Find existing submission or create a new one
$submission = $this->submissionId
? $this->form->submissions()->findOrFail($this->submissionId)
: new FormSubmission();
// Set submission properties
if (!$this->submissionId) {
$submission->form_id = $this->form->id;
}
$submission->data = $formData;
$submission->completion_time = $this->completionTime;
// Set the status based on whether this is a partial submission
$submission->status = $this->isPartial
? FormSubmission::STATUS_PARTIAL
: FormSubmission::STATUS_COMPLETED;
$submission->save();
// Store the submission ID
$this->submissionId = $submission->id;
}
@@ -170,10 +146,8 @@ class StoreFormSubmissionJob implements ShouldQueue
{
$data = $this->submissionData;
$finalData = [];
$properties = collect($this->form->properties);
// Do required transformation per type (e.g. file uploads)
foreach ($data as $answerKey => $answerValue) {
$field = $properties->where('id', $answerKey)->first();
if (!$field) {
@@ -185,45 +159,58 @@ class StoreFormSubmissionJob implements ShouldQueue
|| $field['type'] == 'files'
) {
if (is_array($answerValue)) {
$finalData[$field['id']] = [];
foreach ($answerValue as $file) {
$finalData[$field['id']][] = $this->storeFile($file);
$processedFiles = [];
foreach ($answerValue as $fileItem) {
if (is_string($fileItem) && !empty($fileItem)) {
$singleStoredFile = $this->storeFile($fileItem);
if ($singleStoredFile) {
$processedFiles[] = $singleStoredFile;
}
}
}
$finalData[$field['id']] = $processedFiles;
} else {
$finalData[$field['id']] = $this->storeFile($answerValue);
if (is_string($answerValue) && !empty($answerValue)) {
$singleFileResult = $this->storeFile($answerValue);
$finalData[$field['id']] = $singleFileResult;
} else {
$finalData[$field['id']] = $this->storeFile($answerValue); // Handles null/empty $answerValue by returning null
}
}
} else {
// Standard field processing (text, ID generation, etc.)
if ($field['type'] == 'text' && isset($field['generates_uuid']) && $field['generates_uuid']) {
$finalData[$field['id']] = ($this->form->is_pro) ? Str::uuid()->toString() : 'Please upgrade your OpenForm subscription to use our ID generation features';
} elseif ($field['type'] == 'text' && isset($field['generates_auto_increment_id']) && $field['generates_auto_increment_id']) {
$finalData[$field['id']] = ($this->form->is_pro) ? (string) ($this->form->submissions_count + 1) : 'Please upgrade your OpenForm subscription to use our ID generation features';
} else {
if ($field['type'] == 'text' && isset($field['generates_auto_increment_id']) && $field['generates_auto_increment_id']) {
$finalData[$field['id']] = ($this->form->is_pro) ? (string) ($this->form->submissions_count + 1) : 'Please upgrade your OpenForm subscription to use our ID generation features';
} else {
$finalData[$field['id']] = $answerValue;
}
$finalData[$field['id']] = $answerValue;
}
}
// For Singrature
// Special field types
if ($field['type'] == 'signature') {
$finalData[$field['id']] = $this->storeSignature($answerValue);
}
// For Phone
if ($field['type'] == 'phone_number' && $answerValue && ctype_alpha(substr($answerValue, 0, 2)) && (!isset($field['use_simple_text_input']) || !$field['use_simple_text_input'])) {
$finalData[$field['id']] = substr($answerValue, 2);
}
}
return $finalData;
}
// This is use when updating a record, and file uploads aren't changed.
private function isSkipForUpload($value)
{
$newPath = Str::of(PublicFormController::FILE_UPLOAD_PATH)->replace('?', $this->form->id);
$parser = StorageFileNameParser::parse($value);
$canonicalStoredName = $parser->getMovedFileName();
return Storage::exists($newPath . '/' . $value);
if (!$canonicalStoredName) {
return false; // Input $value couldn't be resolved to a canonical stored name format
}
$destinationPath = Str::of(PublicFormController::FILE_UPLOAD_PATH)->replace('?', $this->form->id);
$fullPathToCheck = $destinationPath . '/' . $canonicalStoredName;
return Storage::exists($fullPathToCheck);
}
/**
@@ -239,61 +226,58 @@ class StoreFormSubmissionJob implements ShouldQueue
if (is_null($value) || empty($value)) {
return null;
}
if (filter_var($value, FILTER_VALIDATE_URL) !== false && str_contains($value, parse_url(config('app.url'))['host'])) { // In case of prefill we have full url so convert to s3
// Handle pre-existing full URLs (e.g., from prefill)
if (filter_var($value, FILTER_VALIDATE_URL) !== false && str_contains($value, parse_url(config('app.url'))['host'])) {
$fileName = explode('?', basename($value))[0];
$path = FormController::ASSETS_UPLOAD_PATH . '/' . $fileName;
$path = FormController::ASSETS_UPLOAD_PATH . '/' . $fileName; // Assuming assets are in a defined path
$newPath = Str::of(PublicFormController::FILE_UPLOAD_PATH)->replace('?', $this->form->id);
Storage::move($path, $newPath . '/' . $fileName);
return $fileName;
}
if ($this->isSkipForUpload($value)) {
return $value;
$shouldSkip = $this->isSkipForUpload($value);
if ($shouldSkip) {
// File (based on canonical name derived from $value) already exists in permanent storage.
// Return its canonical name.
$parser = StorageFileNameParser::parse($value);
return $parser->getMovedFileName() ?? $value; // Fallback to $value if canonical somehow fails (defensive)
}
$fileNameParser = StorageFileNameParser::parse($value);
// Process as a new file upload (or one whose temp version needs to be moved)
$fileNameParser = StorageFileNameParser::parse($value); // $value is the temp file reference (e.g., originalname_uuid.ext or uuid)
if (!$fileNameParser || !$fileNameParser->uuid) {
return null;
return null; // Cannot derive UUID from the reference
}
// Make sure we retrieve the file in tmp storage, move it to persistent
$fileName = PublicFormController::TMP_FILE_UPLOAD_PATH . '/' . $fileNameParser->uuid;
if (!Storage::exists($fileName)) {
// File not found, we skip
return null;
$fileNameInTmp = PublicFormController::TMP_FILE_UPLOAD_PATH . $fileNameParser->uuid;
if (!Storage::exists($fileNameInTmp)) {
return null; // Temporary file not found
}
$movedFileName = $fileNameParser->getMovedFileName(); // This is the canonical name for storage
if (empty($movedFileName)) {
return null; // Canonical name generation failed
}
$newPath = Str::of(PublicFormController::FILE_UPLOAD_PATH)->replace('?', $this->form->id);
$completeNewFilename = $newPath . '/' . $fileNameParser->getMovedFileName();
Log::debug('Moving file to permanent storage.', [
'uuid' => $fileNameParser->uuid,
'destination' => $completeNewFilename,
'form_id' => $this->form->id,
'form_slug' => $this->form->slug,
]);
Storage::move($fileName, $completeNewFilename);
return $fileNameParser->getMovedFileName();
$completeNewFilename = $newPath . '/' . $movedFileName;
Storage::move($fileNameInTmp, $completeNewFilename);
return $movedFileName;
}
private function storeSignature(?string $value)
{
if ($value && preg_match('/^[\/\w\-. ]+$/', $value)) { // If it's filename
return $this->storeFile($value);
// If $value looks like a filename (already processed, e.g. during skip or previous handling)
if ($value && preg_match('/^[\/\w\-. ]+$/', $value)) {
return $this->storeFile($value); // Re-run through storeFile for consistency / skip logic
}
// If $value is base64 data
if ($value == null || !isset(explode(',', $value)[1])) {
return null;
}
$fileName = 'sign_' . (string) Str::uuid() . '.png';
$newPath = Str::of(PublicFormController::FILE_UPLOAD_PATH)->replace('?', $this->form->id);
$completeNewFilename = $newPath . '/' . $fileName;
Storage::put($completeNewFilename, base64_decode(explode(',', $value)[1]));
return $fileName;
}
@@ -304,7 +288,6 @@ class StoreFormSubmissionJob implements ShouldQueue
*/
private function addHiddenPrefills(array &$formData): void
{
// Find hidden properties with prefill, set values
collect($this->form->properties)->filter(function ($property) {
return isset($property['hidden'])
&& isset($property['prefill'])
@@ -331,11 +314,8 @@ class StoreFormSubmissionJob implements ShouldQueue
if ($this->formData === null) {
$this->formData = $this->getFormData();
}
// Ensure the submission ID is included in the returned data
$data = $this->formData;
$data['submission_id'] = $this->submissionId;
return $data;
}
}