statamic / cms

The core Laravel CMS Composer package
https://statamic.com
Other
3.7k stars 508 forks source link

Call to member path() on null in front-end form. #10714

Closed geertjanknapen1 closed 2 weeks ago

geertjanknapen1 commented 2 weeks ago

Bug description

Important, I have seen and looked at #6431 and #7294 which mention the same error message, they do not seem related to this issue.

So, we have custom form handling for our frontend forms. A long story short, it works as follows;

So, explaining out of the way, I use Assets fields in the form, since Statamic does not handle the emailing so I need to store the uploaded files on disk, as per the documentation.

This throws the aforementioned exception in the FormController, but I can't seem to figure out why or what I'm doing wrong, so any nudges in the correct direction would be greatly appreciated.

How to reproduce

I don't think it's easily reproducible, we have a heavily customised setup.

But the jist would be, prevent Statamic from sending the emails by listening for the FormSubmitted event and returning false at the end of it. Create a form with one or more Assets fields. Try to submit that through AJAX with a custom FormController doing as explained in the Bug description.

Logs

[2024-08-28 10:09:55] local.ERROR: Call to a member function path() on null {"userId":"6264c9cd-9c2a-457e-9fce-360708b5aa4d","exception":"[object] (Error(code: 0): Call to a member function path() on null at /usr/share/nginx/html/vendor/statamic/cms/src/Fieldtypes/Assets/Assets.php:143)
[stacktrace]
#0 [internal function]: Statamic\\Fieldtypes\\Assets\\Assets->Statamic\\Fieldtypes\\Assets\\{closure}(false, '\\x00Symfony\\\\Compon...')
#1 /usr/share/nginx/html/vendor/laravel/framework/src/Illuminate/Collections/Arr.php(605): array_map(Object(Closure), Array, Array)
#2 /usr/share/nginx/html/vendor/laravel/framework/src/Illuminate/Collections/Collection.php(785): Illuminate\\Support\\Arr::map(Array, Object(Closure))
#3 /usr/share/nginx/html/vendor/statamic/cms/src/Fieldtypes/Assets/Assets.php(142): Illuminate\\Support\\Collection->map(Object(Closure))
#4 /usr/share/nginx/html/vendor/statamic/cms/src/Fields/Field.php(325): Statamic\\Fieldtypes\\Assets\\Assets->process(Object(Illuminate\\Http\\UploadedFile))
#5 /usr/share/nginx/html/vendor/laravel/framework/src/Illuminate/Collections/HigherOrderCollectionProxy.php(65): Statamic\\Fields\\Field->process()
#6 [internal function]: Illuminate\\Support\\HigherOrderCollectionProxy->Illuminate\\Support\\{closure}(Object(Statamic\\Fields\\Field), 'resume')
#7 /usr/share/nginx/html/vendor/laravel/framework/src/Illuminate/Collections/Arr.php(605): array_map(Object(Closure), Array, Array)
#8 /usr/share/nginx/html/vendor/laravel/framework/src/Illuminate/Collections/Collection.php(785): Illuminate\\Support\\Arr::map(Array, Object(Closure))
#9 /usr/share/nginx/html/vendor/laravel/framework/src/Illuminate/Collections/HigherOrderCollectionProxy.php(64): Illuminate\\Support\\Collection->map(Object(Closure))
#10 /usr/share/nginx/html/vendor/statamic/cms/src/Fields/Fields.php(191): Illuminate\\Support\\HigherOrderCollectionProxy->__call('process', Array)
#11 /usr/share/nginx/html/vendor/statamic/cms/src/Http/Controllers/FormController.php(60): Statamic\\Fields\\Fields->process()
#12 /usr/share/nginx/html/app/Http/Controllers/FormController.php(45): Statamic\\Http\\Controllers\\FormController->submit(Object(Statamic\\Http\\Requests\\FrontendFormRequest), Object(Statamic\\Forms\\Form))
#13 /usr/share/nginx/html/vendor/laravel/framework/src/Illuminate/Routing/Controller.php(54): App\\Http\\Controllers\\FormController->submitStatamicForm(Object(Illuminate\\Http\\Request), 'job_detail_form')
#14 /usr/share/nginx/html/vendor/laravel/framework/src/Illuminate/Routing/ControllerDispatcher.php(43): Illuminate\\Routing\\Controller->callAction('submitStatamicF...', Array)
#15 /usr/share/nginx/html/vendor/laravel/framework/src/Illuminate/Routing/Route.php(260): Illuminate\\Routing\\ControllerDispatcher->dispatch(Object(Illuminate\\Routing\\Route), Object(App\\Http\\Controllers\\FormController), 'submitStatamicF...')
#16 /usr/share/nginx/html/vendor/laravel/framework/src/Illuminate/Routing/Route.php(206): Illuminate\\Routing\\Route->runController()
#17 /usr/share/nginx/html/vendor/laravel/framework/src/Illuminate/Routing/Router.php(808): Illuminate\\Routing\\Route->run()
#18 /usr/share/nginx/html/vendor/laravel/framework/src/Illuminate/Pipeline/Pipeline.php(144): Illuminate\\Routing\\Router->Illuminate\\Routing\\{closure}(Object(Illuminate\\Http\\Request))
#19 /usr/share/nginx/html/app/Http/Middleware/CheckIfAjax.php(23): Illuminate\\Pipeline\\Pipeline->Illuminate\\Pipeline\\{closure}(Object(Illuminate\\Http\\Request))
#20 /usr/share/nginx/html/vendor/laravel/framework/src/Illuminate/Pipeline/Pipeline.php(183): App\\Http\\Middleware\\CheckIfAjax->handle(Object(Illuminate\\Http\\Request), Object(Closure))
#21 /usr/share/nginx/html/vendor/laravel/framework/src/Illuminate/Routing/Middleware/SubstituteBindings.php(51): Illuminate\\Pipeline\\Pipeline->Illuminate\\Pipeline\\{closure}(Object(Illuminate\\Http\\Request))
#22 /usr/share/nginx/html/vendor/laravel/framework/src/Illuminate/Pipeline/Pipeline.php(183): Illuminate\\Routing\\Middleware\\SubstituteBindings->handle(Object(Illuminate\\Http\\Request), Object(Closure))
#23 /usr/share/nginx/html/vendor/laravel/framework/src/Illuminate/Foundation/Http/Middleware/VerifyCsrfToken.php(88): Illuminate\\Pipeline\\Pipeline->Illuminate\\Pipeline\\{closure}(Object(Illuminate\\Http\\Request))
#24 /usr/share/nginx/html/vendor/laravel/framework/src/Illuminate/Pipeline/Pipeline.php(183): Illuminate\\Foundation\\Http\\Middleware\\VerifyCsrfToken->handle(Object(Illuminate\\Http\\Request), Object(Closure))
#25 /usr/share/nginx/html/vendor/laravel/framework/src/Illuminate/View/Middleware/ShareErrorsFromSession.php(49): Illuminate\\Pipeline\\Pipeline->Illuminate\\Pipeline\\{closure}(Object(Illuminate\\Http\\Request))
#26 /usr/share/nginx/html/vendor/laravel/framework/src/Illuminate/Pipeline/Pipeline.php(183): Illuminate\\View\\Middleware\\ShareErrorsFromSession->handle(Object(Illuminate\\Http\\Request), Object(Closure))
#27 /usr/share/nginx/html/vendor/laravel/framework/src/Illuminate/Session/Middleware/StartSession.php(121): Illuminate\\Pipeline\\Pipeline->Illuminate\\Pipeline\\{closure}(Object(Illuminate\\Http\\Request))
#28 /usr/share/nginx/html/vendor/laravel/framework/src/Illuminate/Session/Middleware/StartSession.php(64): Illuminate\\Session\\Middleware\\StartSession->handleStatefulRequest(Object(Illuminate\\Http\\Request), Object(Illuminate\\Session\\Store), Object(Closure))
#29 /usr/share/nginx/html/vendor/laravel/framework/src/Illuminate/Pipeline/Pipeline.php(183): Illuminate\\Session\\Middleware\\StartSession->handle(Object(Illuminate\\Http\\Request), Object(Closure))
#30 /usr/share/nginx/html/vendor/laravel/framework/src/Illuminate/Cookie/Middleware/AddQueuedCookiesToResponse.php(37): Illuminate\\Pipeline\\Pipeline->Illuminate\\Pipeline\\{closure}(Object(Illuminate\\Http\\Request))
#31 /usr/share/nginx/html/vendor/laravel/framework/src/Illuminate/Pipeline/Pipeline.php(183): Illuminate\\Cookie\\Middleware\\AddQueuedCookiesToResponse->handle(Object(Illuminate\\Http\\Request), Object(Closure))
#32 /usr/share/nginx/html/vendor/laravel/framework/src/Illuminate/Cookie/Middleware/EncryptCookies.php(75): Illuminate\\Pipeline\\Pipeline->Illuminate\\Pipeline\\{closure}(Object(Illuminate\\Http\\Request))
#33 /usr/share/nginx/html/vendor/laravel/framework/src/Illuminate/Pipeline/Pipeline.php(183): Illuminate\\Cookie\\Middleware\\EncryptCookies->handle(Object(Illuminate\\Http\\Request), Object(Closure))
#34 /usr/share/nginx/html/vendor/laravel/framework/src/Illuminate/Pipeline/Pipeline.php(119): Illuminate\\Pipeline\\Pipeline->Illuminate\\Pipeline\\{closure}(Object(Illuminate\\Http\\Request))
#35 /usr/share/nginx/html/vendor/laravel/framework/src/Illuminate/Routing/Router.php(807): Illuminate\\Pipeline\\Pipeline->then(Object(Closure))
#36 /usr/share/nginx/html/vendor/laravel/framework/src/Illuminate/Routing/Router.php(786): Illuminate\\Routing\\Router->runRouteWithinStack(Object(Illuminate\\Routing\\Route), Object(Illuminate\\Http\\Request))
#37 /usr/share/nginx/html/vendor/laravel/framework/src/Illuminate/Routing/Router.php(750): Illuminate\\Routing\\Router->runRoute(Object(Illuminate\\Http\\Request), Object(Illuminate\\Routing\\Route))
#38 /usr/share/nginx/html/vendor/laravel/framework/src/Illuminate/Routing/Router.php(739): Illuminate\\Routing\\Router->dispatchToRoute(Object(Illuminate\\Http\\Request))
#39 /usr/share/nginx/html/vendor/laravel/framework/src/Illuminate/Foundation/Http/Kernel.php(201): Illuminate\\Routing\\Router->dispatch(Object(Illuminate\\Http\\Request))
#40 /usr/share/nginx/html/vendor/laravel/framework/src/Illuminate/Pipeline/Pipeline.php(144): Illuminate\\Foundation\\Http\\Kernel->Illuminate\\Foundation\\Http\\{closure}(Object(Illuminate\\Http\\Request))
#41 /usr/share/nginx/html/vendor/statamic/cms/src/Http/Middleware/StopImpersonating.php(12): Illuminate\\Pipeline\\Pipeline->Illuminate\\Pipeline\\{closure}(Object(Illuminate\\Http\\Request))
#42 /usr/share/nginx/html/vendor/laravel/framework/src/Illuminate/Pipeline/Pipeline.php(183): Statamic\\Http\\Middleware\\StopImpersonating->handle(Object(Illuminate\\Http\\Request), Object(Closure))
#43 /usr/share/nginx/html/vendor/statamic/cms/src/Http/Middleware/DisableFloc.php(17): Illuminate\\Pipeline\\Pipeline->Illuminate\\Pipeline\\{closure}(Object(Illuminate\\Http\\Request))
#44 /usr/share/nginx/html/vendor/laravel/framework/src/Illuminate/Pipeline/Pipeline.php(183): Statamic\\Http\\Middleware\\DisableFloc->handle(Object(Illuminate\\Http\\Request), Object(Closure))
#45 /usr/share/nginx/html/vendor/statamic/cms/src/Http/Middleware/CheckMultisite.php(15): Illuminate\\Pipeline\\Pipeline->Illuminate\\Pipeline\\{closure}(Object(Illuminate\\Http\\Request))
#46 /usr/share/nginx/html/vendor/laravel/framework/src/Illuminate/Pipeline/Pipeline.php(183): Statamic\\Http\\Middleware\\CheckMultisite->handle(Object(Illuminate\\Http\\Request), Object(Closure))
#47 /usr/share/nginx/html/vendor/statamic/cms/src/Http/Middleware/CheckComposerJsonScripts.php(21): Illuminate\\Pipeline\\Pipeline->Illuminate\\Pipeline\\{closure}(Object(Illuminate\\Http\\Request))
#48 /usr/share/nginx/html/vendor/laravel/framework/src/Illuminate/Pipeline/Pipeline.php(183): Statamic\\Http\\Middleware\\CheckComposerJsonScripts->handle(Object(Illuminate\\Http\\Request), Object(Closure))
#49 /usr/share/nginx/html/vendor/statamic/cms/src/Http/Middleware/PoweredByHeader.php(18): Illuminate\\Pipeline\\Pipeline->Illuminate\\Pipeline\\{closure}(Object(Illuminate\\Http\\Request))
#50 /usr/share/nginx/html/vendor/laravel/framework/src/Illuminate/Pipeline/Pipeline.php(183): Statamic\\Http\\Middleware\\PoweredByHeader->handle(Object(Illuminate\\Http\\Request), Object(Closure))
#51 /usr/share/nginx/html/vendor/barryvdh/laravel-debugbar/src/Middleware/InjectDebugbar.php(66): Illuminate\\Pipeline\\Pipeline->Illuminate\\Pipeline\\{closure}(Object(Illuminate\\Http\\Request))
#52 /usr/share/nginx/html/vendor/laravel/framework/src/Illuminate/Pipeline/Pipeline.php(183): Barryvdh\\Debugbar\\Middleware\\InjectDebugbar->handle(Object(Illuminate\\Http\\Request), Object(Closure))
#53 /usr/share/nginx/html/app/Http/Middleware/SetDefaultLocaleForUrls.php(38): Illuminate\\Pipeline\\Pipeline->Illuminate\\Pipeline\\{closure}(Object(Illuminate\\Http\\Request))
#54 /usr/share/nginx/html/vendor/laravel/framework/src/Illuminate/Pipeline/Pipeline.php(183): App\\Http\\Middleware\\SetDefaultLocaleForUrls->handle(Object(Illuminate\\Http\\Request), Object(Closure))
#55 /usr/share/nginx/html/vendor/laravel/framework/src/Illuminate/Foundation/Http/Middleware/TransformsRequest.php(21): Illuminate\\Pipeline\\Pipeline->Illuminate\\Pipeline\\{closure}(Object(Illuminate\\Http\\Request))
#56 /usr/share/nginx/html/vendor/laravel/framework/src/Illuminate/Foundation/Http/Middleware/ConvertEmptyStringsToNull.php(31): Illuminate\\Foundation\\Http\\Middleware\\TransformsRequest->handle(Object(Illuminate\\Http\\Request), Object(Closure))
#57 /usr/share/nginx/html/vendor/laravel/framework/src/Illuminate/Pipeline/Pipeline.php(183): Illuminate\\Foundation\\Http\\Middleware\\ConvertEmptyStringsToNull->handle(Object(Illuminate\\Http\\Request), Object(Closure))
#58 /usr/share/nginx/html/vendor/laravel/framework/src/Illuminate/Foundation/Http/Middleware/TransformsRequest.php(21): Illuminate\\Pipeline\\Pipeline->Illuminate\\Pipeline\\{closure}(Object(Illuminate\\Http\\Request))
#59 /usr/share/nginx/html/vendor/laravel/framework/src/Illuminate/Foundation/Http/Middleware/TrimStrings.php(51): Illuminate\\Foundation\\Http\\Middleware\\TransformsRequest->handle(Object(Illuminate\\Http\\Request), Object(Closure))
#60 /usr/share/nginx/html/vendor/laravel/framework/src/Illuminate/Pipeline/Pipeline.php(183): Illuminate\\Foundation\\Http\\Middleware\\TrimStrings->handle(Object(Illuminate\\Http\\Request), Object(Closure))
#61 /usr/share/nginx/html/vendor/laravel/framework/src/Illuminate/Http/Middleware/ValidatePostSize.php(27): Illuminate\\Pipeline\\Pipeline->Illuminate\\Pipeline\\{closure}(Object(Illuminate\\Http\\Request))
#62 /usr/share/nginx/html/vendor/laravel/framework/src/Illuminate/Pipeline/Pipeline.php(183): Illuminate\\Http\\Middleware\\ValidatePostSize->handle(Object(Illuminate\\Http\\Request), Object(Closure))
#63 /usr/share/nginx/html/vendor/laravel/framework/src/Illuminate/Foundation/Http/Middleware/PreventRequestsDuringMaintenance.php(110): Illuminate\\Pipeline\\Pipeline->Illuminate\\Pipeline\\{closure}(Object(Illuminate\\Http\\Request))
#64 /usr/share/nginx/html/vendor/laravel/framework/src/Illuminate/Pipeline/Pipeline.php(183): Illuminate\\Foundation\\Http\\Middleware\\PreventRequestsDuringMaintenance->handle(Object(Illuminate\\Http\\Request), Object(Closure))
#65 /usr/share/nginx/html/vendor/laravel/framework/src/Illuminate/Http/Middleware/HandleCors.php(49): Illuminate\\Pipeline\\Pipeline->Illuminate\\Pipeline\\{closure}(Object(Illuminate\\Http\\Request))
#66 /usr/share/nginx/html/vendor/laravel/framework/src/Illuminate/Pipeline/Pipeline.php(183): Illuminate\\Http\\Middleware\\HandleCors->handle(Object(Illuminate\\Http\\Request), Object(Closure))
#67 /usr/share/nginx/html/vendor/laravel/framework/src/Illuminate/Http/Middleware/TrustProxies.php(57): Illuminate\\Pipeline\\Pipeline->Illuminate\\Pipeline\\{closure}(Object(Illuminate\\Http\\Request))
#68 /usr/share/nginx/html/vendor/laravel/framework/src/Illuminate/Pipeline/Pipeline.php(183): Illuminate\\Http\\Middleware\\TrustProxies->handle(Object(Illuminate\\Http\\Request), Object(Closure))
#69 /usr/share/nginx/html/vendor/laravel/framework/src/Illuminate/Pipeline/Pipeline.php(119): Illuminate\\Pipeline\\Pipeline->Illuminate\\Pipeline\\{closure}(Object(Illuminate\\Http\\Request))
#70 /usr/share/nginx/html/vendor/laravel/framework/src/Illuminate/Foundation/Http/Kernel.php(176): Illuminate\\Pipeline\\Pipeline->then(Object(Closure))
#71 /usr/share/nginx/html/vendor/laravel/framework/src/Illuminate/Foundation/Http/Kernel.php(145): Illuminate\\Foundation\\Http\\Kernel->sendRequestThroughRouter(Object(Illuminate\\Http\\Request))
#72 /usr/share/nginx/html/public/index.php(51): Illuminate\\Foundation\\Http\\Kernel->handle(Object(Illuminate\\Http\\Request))
#73 {main}
"}

Environment

Environment
Application Name: myapp
Laravel Version: 11.21.0
PHP Version: 8.3.0
Composer Version: 2.6.6
Environment: local
Debug Mode: ENABLED
URL: myapp.test
Maintenance Mode: OFF
Timezone: UTC
Locale: en

Cache
Config: NOT CACHED
Events: CACHED
Routes: NOT CACHED
Views: CACHED

Drivers
Broadcasting: log
Cache: file
Database: mysql
Logs: stack / daily
Mail: smtp
Queue: sync
Session: file

Statamic
Addons: 2
Sites: 2 (English, Dutch)
Stache Watcher: Disabled
Static Caching: Disabled
Version: 5.23.0 PRO

Statamic Addons
spatie/statamic-responsive-images: 5.0.0
withcandour/aardvark-seo: 5.0.0

Installation

Existing Laravel app

Additional details

Form blueprint:

tabs:
  main:
    display: Main
    sections:
      -
        fields:
          -
            handle: first_name
            field:
              placeholder: first_name-placeholder
              display: 'First name'
              type: text
              listable: true
              always_save: true
              validate:
                - required
              localizable: true
          -
            handle: last_name
            field:
              placeholder: last_name-placeholder
              display: 'Last name'
              type: text
              listable: true
              always_save: true
              validate:
                - required
              localizable: true
          -
            handle: email
            field:
              placeholder: email-placeholder
              input_type: email
              display: Email
              type: text
              listable: true
              always_save: true
              validate:
                - required
                - email
              localizable: false
          -
            handle: phone
            field:
              placeholder: phone-placeholder
              input_type: tel
              display: 'Phone number'
              type: text
              listable: true
              instructions_position: below
              always_save: true
              validate:
                - required
                - 'min:10'
                - 'max:20'
              localizable: true
          -
            handle: how_did_you_find_us
            field:
              placeholder: how_did_you_find_us-placeholder
              display: 'How did you find us?'
              type: text
              instructions: '(e.g. Job offer page, bus ads, advertisement..)'
              instructions_position: below
              always_save: true
              validate:
                - required
              localizable: false
          -
            handle: resume
            field:
              mode: grid
              container: cvs
              restrict: true
              max_files: 1
              display: 'Upload your CV'
              type: assets
              listable: true
              instructions_position: below
              always_save: true
              validate:
                - required
                - 'mimes:pdf,doc,docx'
              instructions: 'Accepted formats: pdf, doc, docx'
              folder: cvs
              localizable: false
          -
            handle: motivation
            field:
              mode: grid
              container: cvs
              restrict: true
              max_files: 1
              display: 'Upload Your Motivation'
              type: assets
              listable: true
              always_save: true
              validate:
                - 'mimes:pdf,doc,docx'
              folder: cvs
              localizable: false
          -
            handle: retention_period
            field:
              inline: true
              display: 'I consent that my data may be kept up to 1 year after the application date'
              type: checkboxes
              instructions_position: below
              always_save: true
              localizable: false
          -
            handle: general-conditions
            field:
              inline: true
              display: 'I’ve read myapp’s privacy statement and agree to the conditions therein.'
              type: checkboxes
              instructions_position: below
              always_save: false
              validate:
                - required
              localizable: false

FormController (custom):

public function submitStatamicForm(Request $request, string $formHandle)
    {
        // Find the Statamic form by its handle.
        $form = Form::find($formHandle);

        if (is_null($form)) {
            return response()->json(['message' => "Form '$formHandle' not found"], Response::HTTP_NOT_FOUND);
        }

        // Statamic expects a FrontendFormRequest class for form submissions, so create one from request
        $frontendFormRequest = FrontendFormRequest::createFrom($request);

        // Submit the form through Statamics' 'submit' method.
        $response = parent::submit($frontendFormRequest, $form);

        // On success, update the content to include a rendered form confirmation view.
        // The view will have access to the "confirmation" entry and to the values submitted in the form.
        if ($response->getStatusCode() === Response::HTTP_OK) {
            $responseContent = $response->getOriginalContent();
            $confirmationSlug = Str::replace('_', '-', $formHandle);
            $confirmation = $this->formService->getFormConfirmation($confirmationSlug, $request['locale'] ?? Locale::ENGLISH->value);

            if (is_null($confirmation)) {
                return response()->json(['message' => "Form confirmation '$confirmationSlug' not found"], Response::HTTP_NOT_FOUND);
            }

            $successView = StatamicView::make(
                $confirmation->originValue('template'),
                [
                    'form' => $responseContent,
                    'confirmation' => $confirmation,
                ]
            );

            $responseContent['renderedSuccessView'] = $successView->render();
            $response->setContent($responseContent);
        }

        return $response;
    }
geertjanknapen1 commented 2 weeks ago

I did try using the Files fieldtypes, and this does not throw an exception, but as we handle mailing ourselves, the proper files are not added as an attachment as the documentation states.

Additional information regarding how you end up in the custom FormController. Routing

 Route::controller(FormController::class)
    ->middleware(['ajax']) # Returns 403 error if !$request->ajax(), otherwise continues.
    ->group(function () {
        Route::post('/ajaxforms/{formHandle}', 'submitStatamicForm')->name('submit-statamic-form');
    });

AJAX form submission

'use strict'

// Define the 'forms' namespace
if (!('forms' in window)) window['forms'] = [];

$(function() {
    $('body').on('click', '.form-ajax-submit', function (e) {
        e.preventDefault();

        forms.disableSubmit(e); // Disables the submit button after user clicked it, not relevant for issue.
        forms.ajaxSubmit(e);
    });
});

/*******************************
 * Methods ('forms' namespace) *
 *******************************/

forms.ajaxSubmit = function (e) {
    let form = $(e.target).closest('form');
    let formId = form.attr('id');
    let formName = form.attr('name');
    let formErrorSpan = form.find('span[data-failure]');
    let failureMessage = formErrorSpan.data('failure');

    // clear potential previously set validation errors
    // forms.clearValidationErrors(form); // For this issue you can assume no validation errors happen.

    // show spinning loader
    $('#envelope-icon').addClass('d-none');
    $('#spinning-loader').removeClass('d-none');

    let formDataForm = $('form#' + formId)[0];
    let formData = new FormData(formDataForm);

    // If the form contains the resume fields (file) we want to append both the resume and motivation files to the FormData
    if ($('input[name="resume"]').length > 0) {
        formData.append('resume', $('input[name="resume"]')[0].files[0]);
        formData.append('motivation', $('input[name="motivation"]')[0].files[0]);
    }

    $.ajax('/ajaxforms/' + formName, {
        type: 'POST',
        processData: false,
        contentType: false,
        data: formData,
    })
        .done(function (response) {
            let formContainer = form.closest('div');

            $(formContainer).html(response.renderedSuccessView);
            forms.enableSubmit(e);
        })
        .fail(function (response) {
            if (response.status === 404) {
                console.log(response.responseJSON.message);

                formErrorSpan.html(failureMessage);
                formErrorSpan.show();
            } else {
                $.each(response.responseJSON.error, function (formFieldName, validationErrorText) {
                    let formField = form.find('[name=' + formFieldName + ']');
                    let formFieldValidationSpan = form.find('span.' + formFieldName);

                    formFieldValidationSpan.html(validationErrorText);
                    formFieldValidationSpan.show();
                    formField.addClass('is-invalid');
                });
            }

            forms.enableSubmit(e); // Enables the submit button again, not relevant for issue
        });

Below is the code for the listener, it uses Enums to check the form handle which should not be relevant to the issue. The handleFormSubmission is part of a Trait and just sends the Mailables to the correct location

public function handle(FormSubmitted $event)
    {
        if ($event->submission->form->handle() === Form::APPLICATION_FORM->value) {
            // Handle form submission (send mail and store submitted data)
            $this->handleFormSubmission($event, ApplicationFormInternalMailable::class, ApplicationFormExternalMailable::class);

            // return false so Statamic does not send the e-mail (this also stops propagation of this event to other listeners!!).
            return false;
        }
        // return nothing so that propagation is not stopped unless another listener picks up and handles this event
    }
duncanmcclean commented 2 weeks ago

Has this only just started happening recently or have you just built this and it's not working?

geertjanknapen1 commented 2 weeks ago

Has this only just started happening recently or have you just built this and it's not working?

Sad to say I don't really have a proper answer for this. We utilize this setup on another website, but that website does not have any file-upload fields in the forms.

This website used to run on Statamic 3, so I am now updating it. (Yes, it was greatly neglected) I followed the guides from 3 to 4 and 4 to 5, and Control Panel, and the whole shebang seems to work fine.

The form setup used to be less advanced (just the listener, no AJAX submission, and custom form controller), and when it was it worked. properly.

But we want this website to be in line with our more advanced website, thus also utilizing the more advanced form setup.


Old listener:

public function handle(FormSubmitted $event)
    {
        if ($event->submission->form->handle() === Form::APPLICATION_FORM->value) {
            $emailJob =  new SendEmail($event->submission, Site::current(), $event->submission->form->email()[0]);
            dispatch($emailJob)->onQueue('every_minute');

            // return false so Statamic does not send the e-mail (this also stops propagation of this event to other listeners!!).
            return false;
        }

        // return nothing so that propagation is not stopped unless another listener picks up and handles this event
    }

If you need any other information please feel free to ask. And thanks for putting up with my shenanigans again @duncanmcclean , it's much appreciated as always!

geertjanknapen1 commented 2 weeks ago

@duncanmcclean I'll take a look if I can reproduce the error on the website where this setup is working tomorrow. That would probably narrow it down to some configuration issues on our end.

duncanmcclean commented 2 weeks ago

We don't usually recommend that you hijack our controllers like you have. 😄

Although, if you're able to reproduce the issue on a fresh site or provide access to the site you're having the issues on, then I can take a look.

geertjanknapen1 commented 2 weeks ago

We don't usually recommend that you hijack our controllers like you have. 😄

Although, if you're able to reproduce the issue on a fresh site or provide access to the site you're having the issues on, then I can take a look.

Hijacking is a strong word, I prefer borrowing 😄

I'll report back either tomorrow or the day after, have a good one!

geertjanknapen1 commented 2 weeks ago

We don't usually recommend that you hijack our controllers like you have. 😄

Although, if you're able to reproduce the issue on a fresh site or provide access to the site you're having the issues on, then I can take a look.

I tried on our other site, no cigar. Same issue, Then I tried on a new Statamic install, same thing.

But your hijacking comment made me wonder, what is the recommended way to take over the whole mailing, without hijacking Statamic's controllers and everything?

Is there something in the docs that I'm missing, where this is thoroughly explained, and I just missed it or something?


I'll try and create a Github repo with my (very very ugly) minimal reproducible example.

I've created a Github repo with my (very very ugly) minimal reproducible example, find it here

Can't give you access to the other site. Or well, I could but the content for that site is not in the same git repo as the project's code, so it'd be a major hassle to setup.

duncanmcclean commented 2 weeks ago

But your hijacking comment made me wonder, what is the recommended way to take over the whole mailing, without hijacking Statamic's controllers and everything?

I don't think we really have a recommended way to not use our native mailing, other than not configuring any emails.

(I've created a Github repo with my (very very ugly) minimal reproducible example, find it here

Thanks, I'll take a look when I get a chance.

duncanmcclean commented 2 weeks ago

Just looking at the code, I can get the assets to upload, but they then get deleted if you return false from the FormSubmitted listener (intentionally).

Why can't you use the normal Statamic email logic?

geertjanknapen1 commented 2 weeks ago

That makes sense I guess..

Well, we've created a collection where our Content department can create dynamic email templates in all the languages that are supported on the site. When a form is submitted, we listen for it, take the content from the correct email template for the type of form that was submitted, toss that content in a mailable, and then use our custom MailJob to send the email.

Then the contacts are subscribed to one or more lists in Mailchimp, again, based on the type of form that was submitted.

Since it's quite conditional what is supposed to happen while certain forms are submitted, we thought (and subsequently decided) that it would be better to handle the mailing ourselves so we have full control over what happens. But it seems we've shot ourselves in the foot in that regard.

But I guess this issue boils down to "You're doing it wrong"?

geertjanknapen1 commented 2 weeks ago

@duncanmcclean Does Statamic's mailing logic support queues?

This is one of the other reasons we want to 'hijack' the mailing from Statamic :)

duncanmcclean commented 2 weeks ago

When a form is submitted, we listen for it, take the content from the correct email template for the type of form that was submitted, toss that content in a mailable, and then use our custom MailJob to send the email.

We have a config option which'll let you use your own class for sending emails: https://github.com/statamic/cms/blob/5.x/config/forms.php#L36

You could do all of your "which email template" logic in there, as well as determining if emails should actually get sent or not.

Then the contacts are subscribed to one or more lists in Mailchimp, again, based on the type of form that was submitted.

You can continue to listen to the FormSubmitted event for handling this.

Does Statamic's mailing logic support queues?

Yes, as long as you have a queue enabled, it should send emails using the queue.

But I guess this issue boils down to "You're doing it wrong"?

Yeah, sorry! We never really intended for people to do what you're doing.