2022-09-20 21:59:52 +02:00
< ? php
namespace App\Jobs\Form ;
use App\Events\Forms\FormSubmitted ;
2023-10-20 11:00:35 +02:00
use App\Http\Controllers\Forms\FormController ;
2024-02-23 11:54:12 +01:00
use App\Http\Controllers\Forms\PublicFormController ;
2022-09-20 21:59:52 +02:00
use App\Http\Requests\AnswerFormRequest ;
use App\Models\Forms\Form ;
2023-01-10 14:52:14 +01:00
use App\Models\Forms\FormSubmission ;
2023-11-28 11:31:57 +01:00
use App\Service\Forms\FormLogicPropertyResolver ;
2022-09-20 21:59:52 +02:00
use App\Service\Storage\StorageFileNameParser ;
use Illuminate\Bus\Queueable ;
use Illuminate\Contracts\Queue\ShouldQueue ;
use Illuminate\Foundation\Bus\Dispatchable ;
use Illuminate\Queue\InteractsWithQueue ;
use Illuminate\Queue\SerializesModels ;
use Illuminate\Support\Facades\Storage ;
use Illuminate\Support\Str ;
2025-04-28 17:33:55 +02:00
use Illuminate\Support\Facades\Log ;
/**
* Job to store form submissions
*
* This job handles the storage of form submissions , including processing of metadata
* and special field types like files and signatures .
*
* The job accepts all data in the submissionData array , including metadata fields :
* - submission_id : ID of an existing submission to update ( must be an integer )
* - completion_time : Time in seconds it took to complete the form
* - is_partial : Whether this is a partial submission ( will be stored with STATUS_PARTIAL )
* If not specified , submissions are treated as complete by default .
*
* These metadata fields will be automatically extracted and removed from the stored form data .
*
* For partial submissions :
* - The submission will be stored with STATUS_PARTIAL
* - All file uploads and signatures will be processed normally
* - The submission can later be updated to STATUS_COMPLETED when the user completes the form
*/
2022-09-20 21:59:52 +02:00
class StoreFormSubmissionJob implements ShouldQueue
{
2024-02-23 11:54:12 +01:00
use Dispatchable ;
use InteractsWithQueue ;
use Queueable ;
use SerializesModels ;
2022-09-20 21:59:52 +02:00
2025-04-28 17:33:55 +02:00
public ? int $submissionId = null ;
2025-02-01 22:52:06 +01:00
private ? array $formData = null ;
2025-04-28 17:33:55 +02:00
private ? int $completionTime = null ;
private bool $isPartial = false ;
2023-01-10 14:52:14 +01:00
2022-09-20 21:59:52 +02:00
/**
* Create a new job instance .
*
2025-04-28 17:33:55 +02:00
* @ param Form $form The form being submitted
* @ param array $submissionData Form data including metadata fields ( submission_id , completion_time , etc . )
2022-09-20 21:59:52 +02:00
* @ return void
*/
2025-04-28 17:33:55 +02:00
public function __construct ( public Form $form , public array $submissionData )
2022-09-20 21:59:52 +02:00
{
}
/**
* Execute the job .
*
* @ return void
*/
public function handle ()
{
2025-04-28 17:33:55 +02:00
// Extract metadata from submission data
$this -> extractMetadata ();
// Process form data
2025-02-01 22:52:06 +01:00
$this -> formData = $this -> getFormData ();
$this -> addHiddenPrefills ( $this -> formData );
2022-09-20 21:59:52 +02:00
2025-04-28 17:33:55 +02:00
// Store the submission
2025-02-01 22:52:06 +01:00
$this -> storeSubmission ( $this -> formData );
2022-09-20 21:59:52 +02:00
2025-04-28 17:33:55 +02:00
// Add the submission ID to the form data after storing the submission
2025-02-01 22:52:06 +01:00
$this -> formData [ 'submission_id' ] = $this -> submissionId ;
2022-09-20 21:59:52 +02:00
2025-04-28 17:33:55 +02:00
// Only trigger integrations for completed submissions, not partial ones
if ( ! $this -> isPartial ) {
FormSubmitted :: dispatch ( $this -> form , $this -> formData );
}
2023-01-10 14:52:14 +01:00
}
2025-04-28 17:33:55 +02:00
/**
* Extract metadata from submission data
*
* This method extracts and removes metadata fields from the submission data :
* - submission_id
* - completion_time
* - is_partial
*/
private function extractMetadata () : void
2024-02-03 12:50:57 +01:00
{
2025-04-28 17:33:55 +02:00
// Extract completion time
if ( isset ( $this -> submissionData [ 'completion_time' ])) {
$this -> completionTime = $this -> submissionData [ 'completion_time' ];
unset ( $this -> submissionData [ 'completion_time' ]);
}
2024-02-23 11:54:12 +01:00
2025-04-28 17:33:55 +02:00
// 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' ]);
}
2024-02-03 12:50:57 +01:00
}
2025-04-28 17:33:55 +02:00
/**
* Get the submission ID
*
* @ return int | null
*/
public function getSubmissionId ()
2023-01-10 14:52:14 +01:00
{
2025-04-28 17:33:55 +02:00
return $this -> submissionId ;
2023-01-10 14:52:14 +01:00
}
/**
2025-04-28 17:33:55 +02:00
* Store the submission in the database
*
* @ param array $formData
2023-01-10 14:52:14 +01:00
*/
2025-04-28 17:33:55 +02:00
private function storeSubmission ( array $formData )
2023-01-10 14:52:14 +01:00
{
2025-04-28 17:33:55 +02:00
// 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 ;
2024-02-03 12:50:57 +01:00
}
2024-02-23 11:54:12 +01:00
2025-04-28 17:33:55 +02:00
$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 ();
2023-01-10 14:52:14 +01:00
2025-04-28 17:33:55 +02:00
// Store the submission ID
$this -> submissionId = $submission -> id ;
2023-01-10 14:52:14 +01:00
}
2022-09-20 21:59:52 +02:00
/**
* Retrieve data from request object , and pre - format it if needed .
* - Replace notionforms id with notion field ids
* - Clean \ in select id values
* - Stores file and replace value with url
* - Generate auto increment id & unique id features for rich text field
*/
private function getFormData ()
{
$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 ();
2024-04-15 15:12:36 +02:00
if ( ! $field ) {
2022-09-20 21:59:52 +02:00
continue ;
}
if (
( $field [ 'type' ] == 'url' && isset ( $field [ 'file_upload' ]) && $field [ 'file_upload' ])
2024-04-15 15:12:36 +02:00
|| $field [ 'type' ] == 'files'
) {
2022-09-20 21:59:52 +02:00
if ( is_array ( $answerValue )) {
$finalData [ $field [ 'id' ]] = [];
foreach ( $answerValue as $file ) {
$finalData [ $field [ 'id' ]][] = $this -> storeFile ( $file );
}
} else {
$finalData [ $field [ 'id' ]] = $this -> storeFile ( $answerValue );
}
} else {
if ( $field [ 'type' ] == 'text' && isset ( $field [ 'generates_uuid' ]) && $field [ 'generates_uuid' ]) {
2025-04-02 11:10:35 +02:00
$finalData [ $field [ 'id' ]] = ( $this -> form -> is_pro ) ? Str :: uuid () -> toString () : 'Please upgrade your OpenForm subscription to use our ID generation features' ;
2022-09-20 21:59:52 +02:00
} else {
if ( $field [ 'type' ] == 'text' && isset ( $field [ 'generates_auto_increment_id' ]) && $field [ 'generates_auto_increment_id' ]) {
2024-02-23 11:54:12 +01:00
$finalData [ $field [ 'id' ]] = ( $this -> form -> is_pro ) ? ( string ) ( $this -> form -> submissions_count + 1 ) : 'Please upgrade your OpenForm subscription to use our ID generation features' ;
2022-09-20 21:59:52 +02:00
} else {
$finalData [ $field [ 'id' ]] = $answerValue ;
}
}
}
2022-12-22 11:53:33 +01:00
// For Singrature
2024-04-18 10:29:02 +02:00
if ( $field [ 'type' ] == 'signature' ) {
2022-12-22 11:53:33 +01:00
$finalData [ $field [ 'id' ]] = $this -> storeSignature ( $answerValue );
}
2023-10-03 17:50:46 +02:00
// For Phone
2024-04-15 15:12:36 +02:00
if ( $field [ 'type' ] == 'phone_number' && $answerValue && ctype_alpha ( substr ( $answerValue , 0 , 2 )) && ( ! isset ( $field [ 'use_simple_text_input' ]) || ! $field [ 'use_simple_text_input' ])) {
2023-10-03 17:50:46 +02:00
$finalData [ $field [ 'id' ]] = substr ( $answerValue , 2 );
}
2022-09-20 21:59:52 +02:00
}
return $finalData ;
}
2023-04-26 17:05:02 +02:00
// 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 );
2024-02-23 11:54:12 +01:00
2024-04-15 15:12:36 +02:00
return Storage :: exists ( $newPath . '/' . $value );
2023-04-26 17:05:02 +02:00
}
2022-09-20 21:59:52 +02:00
/**
* Custom Back - end Value formatting . Use case :
* - File uploads ( move file from tmp storage to persistent )
*
* File can have 2 formats :
* - file_name - { uuid } . { ext }
* - { uuid }
*/
2025-03-25 18:35:16 +01:00
private function storeFile ( $value , ? bool $isPublic = null )
2022-09-20 21:59:52 +02:00
{
2025-03-25 18:35:16 +01:00
if ( is_null ( $value ) || empty ( $value )) {
2022-09-20 21:59:52 +02:00
return null ;
}
2024-02-23 11:54:12 +01:00
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
2024-04-15 15:12:36 +02:00
$fileName = explode ( '?' , basename ( $value ))[ 0 ];
$path = FormController :: ASSETS_UPLOAD_PATH . '/' . $fileName ;
2023-10-20 11:00:35 +02:00
$newPath = Str :: of ( PublicFormController :: FILE_UPLOAD_PATH ) -> replace ( '?' , $this -> form -> id );
2024-04-15 15:12:36 +02:00
Storage :: move ( $path , $newPath . '/' . $fileName );
2024-02-23 11:54:12 +01:00
2023-10-20 11:00:35 +02:00
return $fileName ;
}
2024-01-11 14:07:27 +01:00
2024-02-23 11:54:12 +01:00
if ( $this -> isSkipForUpload ( $value )) {
2023-04-26 17:05:02 +02:00
return $value ;
}
2022-09-20 21:59:52 +02:00
$fileNameParser = StorageFileNameParser :: parse ( $value );
2025-03-25 18:35:16 +01:00
if ( ! $fileNameParser || ! $fileNameParser -> uuid ) {
return null ;
}
2022-09-20 21:59:52 +02:00
// Make sure we retrieve the file in tmp storage, move it to persistent
2024-04-15 15:12:36 +02:00
$fileName = PublicFormController :: TMP_FILE_UPLOAD_PATH . '/' . $fileNameParser -> uuid ;
if ( ! Storage :: exists ( $fileName )) {
2022-09-20 21:59:52 +02:00
// File not found, we skip
return null ;
}
$newPath = Str :: of ( PublicFormController :: FILE_UPLOAD_PATH ) -> replace ( '?' , $this -> form -> id );
2024-04-15 15:12:36 +02:00
$completeNewFilename = $newPath . '/' . $fileNameParser -> getMovedFileName ();
2022-09-20 21:59:52 +02:00
2025-04-28 17:33:55 +02:00
Log :: debug ( 'Moving file to permanent storage.' , [
2022-09-20 21:59:52 +02:00
'uuid' => $fileNameParser -> uuid ,
'destination' => $completeNewFilename ,
'form_id' => $this -> form -> id ,
'form_slug' => $this -> form -> slug ,
]);
2023-08-16 10:59:07 +02:00
Storage :: move ( $fileName , $completeNewFilename );
2022-09-20 21:59:52 +02:00
return $fileNameParser -> getMovedFileName ();
}
2022-12-22 11:53:33 +01:00
private function storeSignature ( ? string $value )
{
2024-08-16 16:49:23 +02:00
if ( $value && preg_match ( '/^[\/\w\-. ]+$/' , $value )) { // If it's filename
return $this -> storeFile ( $value );
}
2024-04-15 15:12:36 +02:00
if ( $value == null || ! isset ( explode ( ',' , $value )[ 1 ])) {
2022-12-22 11:53:33 +01:00
return null ;
}
2024-04-15 15:12:36 +02:00
$fileName = 'sign_' . ( string ) Str :: uuid () . '.png' ;
2022-12-22 11:53:33 +01:00
$newPath = Str :: of ( PublicFormController :: FILE_UPLOAD_PATH ) -> replace ( '?' , $this -> form -> id );
2024-04-15 15:12:36 +02:00
$completeNewFilename = $newPath . '/' . $fileName ;
2022-12-22 11:53:33 +01:00
2023-08-16 10:59:07 +02:00
Storage :: put ( $completeNewFilename , base64_decode ( explode ( ',' , $value )[ 1 ]));
2023-01-10 14:52:14 +01:00
2022-12-22 11:53:33 +01:00
return $fileName ;
}
2022-09-20 21:59:52 +02:00
/**
* Adds prefill from hidden fields
*
* @ param AnswerFormRequest $request
*/
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' ])
2023-11-28 11:31:57 +01:00
&& FormLogicPropertyResolver :: isHidden ( $property , $this -> submissionData )
2024-04-15 15:12:36 +02:00
&& ! is_null ( $property [ 'prefill' ])
&& ! in_array ( $property [ 'type' ], [ 'files' ])
&& ! ( $property [ 'type' ] == 'url' && isset ( $property [ 'file_upload' ]) && $property [ 'file_upload' ]);
2022-09-20 21:59:52 +02:00
}) -> each ( function ( array $property ) use ( & $formData ) {
if ( $property [ 'type' ] === 'date' && isset ( $property [ 'prefill_today' ]) && $property [ 'prefill_today' ]) {
2023-06-30 15:52:50 +02:00
$formData [ $property [ 'id' ]] = now () -> format (( isset ( $property [ 'with_time' ]) && $property [ 'with_time' ]) ? 'Y-m-d H:i' : 'Y-m-d' );
2022-09-20 21:59:52 +02:00
} else {
$formData [ $property [ 'id' ]] = $property [ 'prefill' ];
}
});
}
2025-02-01 22:52:06 +01:00
/**
2025-04-28 17:33:55 +02:00
* Get the processed form data including the submission ID
*
* @ return array
2025-02-01 22:52:06 +01:00
*/
public function getProcessedData () : array
{
if ( $this -> formData === null ) {
$this -> formData = $this -> getFormData ();
}
2025-04-28 17:33:55 +02:00
// Ensure the submission ID is included in the returned data
$data = $this -> formData ;
$data [ 'submission_id' ] = $this -> submissionId ;
return $data ;
2025-02-01 22:52:06 +01:00
}
2022-09-20 21:59:52 +02:00
}