Custom Form Handler in Statamic

Posted on

This past month I've been working on some pretty heavy forms in Statamic that led me down the path to some custom Statamic addon development.

The Statamic docs are generally excellent 99% of the time, but there's nothing in the API docs around form handling. So for that kind of work, you're needing to grok core.

Here was my MO with form submissions:

Use a standard Statamic form tag and fieldset. Take the form submission, store the input on the file system but also pass the form submission to a third-party CRM. The response from the CRM will be a redirect URL; Statamic should redirect the user to it.

We know that Statamic creates files from form submissions natively - if you've enabled that in the form settings but how do we pass the submission to the CRM?

My initial thought and process was to create a form listener. On terminal, you'd do:

php please make:listener MyForms

This scaffolds the listener for me.

<?php

namespace Statamic\Addons\MyForms;

use Statamic\Extend\Listener;

class MyFormsListener extends Listener
{
    /**
     * The events to be listened for, and the methods to call.
     *
     * @var array
     */
    public $events = [];
}

The docs provide a good list of Statamic events but the one I was going to need specifically was Form.submission.creating.

So changing MyFormsListener.php to this

<?php
namespace Statamic\Addons\FormProcessor;
use GuzzleHttp\Client;
use Illuminate\Support\Facades\Log;
use Statamic\Contracts\Forms\Submission;
use Statamic\Extend\Listener;
class FormProcessorListener extends Listener
{
    /**
     * The events to be listened for, and the methods to call.
     *
     * @var array
     */
    public $events = [
        'Form.submission.creating' => 'submitEntry'
    ];
    public function submitEntry(Submission $submission)
    {
        // get the formset from the submission
        $form = $submission->formset();
        $form_name = $form->name();
        
        // get an array of available forms from the addon settings
        $forms = $this->getConfig('formsets');
        
        // find the settings for the submitted form
        $key = array_search($form_name, array_column($forms, 'formset'));
        
        // get the CRM API key for this form
        $endpoint_key = $forms[$key]['form_endpoint_key'];
        
        // make a Guzzle request to the API
        $client = new Client([
            'base_uri' => env('FORM_API_BASE_URL'),
            'defaults' => [
                'exceptions' => false
            ]
        ]);
        
        // setup our form data as Guzzle form params
        $options = [
            'form_params' => $submission->data()
        ];
        
        // try post submission to the CRM
        try {
            $response = $client->post($endpoint_key, $options);
            $redirect_url = $response->getBody()->getContents();
            return $redirect_url;
        } catch (RequestException $e) {
            // Log if it can't
            Log::critical('There was an issue passing the form to the CRM');
            return env('FORM_DEFAULT_REDIRECT_URL');
        }
        
    }
}

This got me 90% of the way. The CRM was happy, it got the food it needed, it sent the response but nothing happened on the Statamic side 🤔.

Of course, the listener was doing just that, listening for a submission event, Statamic core was handling the rest of the submission process, creating the file, saving to the file system etc.

So, back to the drawing board.

After a post on the forum and a helpful nudge in the right direction from @erin - such a great guy btw - I opted for a custom controller.

php please make:controller MyForms

The Statamic docs on controllers are great.

I moved my logic from MyFormsListener.php method into a new postSubmission() method inside MyFormsController.php but that doesn't handle creating the form submission.

As I mentioned earlier, you need to grok the core to see how the Statamic Gents handle form submissions. You'll find that in FormListener.php and specifically the create() method.

Taking what's in that method and adding it to my postSubmission() method, specifically lines 29-40 and then 49-64

<?php
namespace Statamic\Addons\FormProcessor;
use GuzzleHttp\Client;
use GuzzleHttp\Exception\RequestException;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Log;
use Statamic\API\Form;
use Statamic\Exceptions\PublishException;
use Statamic\Exceptions\SilentFormFailureException;
use Statamic\Extend\Controller;
class FormProcessorController extends Controller
{
    /**
     * Maps to your route definition in routes.yaml
     *
     * @return mixed
     */
    public function index()
    {
        return $this->view('index');
    }
    public function postSubmission(Request $request)
    {
        // get the formset from the request
        // a hidden field that has the formset value
        $formset = $request->formset;
        
        // get the data from the submission
        $fields = \Statamic\API\Request::all();
        
        // get the right form
        $form = Form::get($formset);
        
        // get the forms from addon setttings
        $forms = $this->getConfig('formsets');
        $key = array_search($form, array_column($forms, 'formset'));
        $endpoint_key = $forms[$key]['form_endpoint_key'];
        // create the form submission
        $submission = $form->createSubmission();
        try {
            // assign the submission data to the form
            $submission->data($fields);
            $submission->uploadFiles();
        } catch (PublishException $e) {
            return $this->formFailure($e->getErrors(), $formset);
        } catch (SilentFormFailureException $e) {
            return $this->formSuccess($formset, $submission);
        }
        if ($form->shouldStore()) {
            // if the form is set to store submissions
            // store the file to the filesystem
            $submission->save();
        }
        
        // continue with our CRM logic
        $client = new Client([
            'base_uri' => env('FORM_API_BASE_URL'),
            'defaults' => [
                'exceptions' => false
            ]
        ]);
        $options = [
            'form_params' => $submission->data()
        ];
        try {
            $response = $client->post($endpoint_key, $options);
            $redirect_url = $response->getBody()->getContents();
            return $redirect_url;
        } catch (RequestException $e) {
            Log::critical('There was an issue passing the form to CRM');
            return env('FORM_DEFAULT_REDIRECT_URL');
        }
    }
}

I should probably mention at this point, my forms are submitted via Vue methods and that's why I'm returning a String value.

If you were handling things server side then you'd do this on line 84.

return redirect()->away($redirect_url);

So there you have it. A custom form handler that takes the submission, saves the data as a flat file on the file system, tries to pass it to a CRM. If successful, we get a redirect URL in response that we can use to redirect the user. If it fails, we log a critical error to notify the client that something went wrong, we still have the form data saved so that we can manually update the CRM and we redirect the user to a fallback URL.

Job done 👍🏼

Thanks for reading.