Intro to Custom Form Handling in Statamic

Statamic's built in forms are one of the great features that make it such a compelling system to work with. Without touching the code you can create your forms and collect entries in the back end or have them sent to you via e-mail.

But what if you want some other, more advanced functionality? Maybe you want to sign the user up to your third party mailing list tool. Or you have to pull in some external data depending on their input in order to calculate and return some sort of complex result.

In this case you need to implement your own form handling. Let's see how we can achieve just that by creating a simple newsletter signup form that sends the input data to an external API, something I just had to do for a client last month.

The Signup Form

We will keep this form minimal, both to keep this example simple and to save the user from having to input too much of his personal information. So let's just stick with two fields: one required input for the e-mail address and one optional input for the user's name.

In a real world scenario you'd definitely have to also add some sort of privacy policy checkbox, and maybe you'd also want to add some hidden fields with information about the current site or the user intent so you can put them into the right user segment.

You can easily create your form using the control panel - here is how the finished blueprint will look like in the yaml file.

sections: main: display: Main fields: - handle: email field: input_type: email antlers: false display: Email type: text icon: text listable: hidden instructions_position: above validate: - required - handle: name field: input_type: text antlers: false display: Name type: text icon: text listable: hidden instructions_position: above
The blueprint for our newsletter signup form

Once you have created the form and its blueprint, you can add it to your website and Statamic will automatically start collecting the submission data in the filesystem. But since that is not where we need the data, we will now introduce our custom form handling instead.

Listening to Statamic's Events

As a first step we need to find a way to execute our custom code every time our form is submitted by a user.

To make this process as painless as possible, Statamic follows Laravel's conventions and dispatches events whenever certain actions are taken. Have a look at the list of available events in the documentation to get an idea of all the possibilities.

Going through the list we can find two events that are potentially of interest for our use case:

  • FormSubmitted: this is dispatched as soon as the user submits the form, after Statamic validates the input but before it stores or emails the submitted data

  • SubmissionCreated: once Statamic has accepted the user's input and created a submission record in the back end, this event is triggered

Our example signup form doesn't need Statamic to store or otherwise handle the user input, since all that is handled externally, so the FormSubmitted event looks like a better fit for this scenario.

Creating an Event Listener

Now that we know the event we are looking for, we have to create a custom listener that will be triggered whenever the event occurs. Simply switch to your terminal and run php please make:listener NewsletterFormListener --event=FormSubmitted.

This will do two things for you: first it will create a new NewsletterFormListener.php in the event listener section of your Laravel installation, which will handle our functionality. Here is what that file looks like without any modifications:

<?php namespace App\Listeners; use Statamic\Events\FormSubmitted; use Illuminate\Contracts\Queue\ShouldQueue; use Illuminate\Queue\InteractsWithQueue; class NewsletterFormListener { /** * Create the event listener. * * @return void */ public function __construct() { // } /** * Handle the event. * * @param FormSubmitted $event * @return void */ public function handle(FormSubmitted $event) { // } }
The base event listener created by the make:listener command

Registering the Listener

Secondly the above command will also make a change to your EventServiceProvider.php to register your new listener.

protected $listen = [ FormSubmitted::class => [ NewsletterFormListener::class, ], /* other entries */ ];
Registering your listener in the EventServiceProvider

This little snippets lets your application know which class to call - your new listener - when the event occurs. That's it for the event handling - we can go right into creating the functionality we need.

Adding the Custom Form Handling

Looking at your newly created NewsletterFormListener.php you will notice it only comes with two predefined functions, a constructor and a handle method. As the name implies, we use handle() to handle our form submission.

Accessing the Submission Data

Looking at the file you will see that the function is passed an event object $event, which contains all of the information about the event including the data entered into the form.

Let's have a look at the data that's available to us. Since the event handler is triggered in the background, we can't just print or return the data to the user, but we can just write it to the log instead.

/app/listeners/NewsletterFormListener.php (excerpt)
public function handle(FormSubmitted $event) { Log::debug(serialize($event)); return false; }
Printing the received FormSubmitted event object to the log

Here's how that output looks after adding some line breaks to make it easier to read. Note that any property with a * in front (e.g. *data) is protected and not directly accessible, so we will need to use getter-methods for those.

/storage/logs/laravel.log (excerpt)
[2022-03-01 10:13:38] local.DEBUG: O:29:"Statamic\Events\FormSubmitted":2:{ s:10:"submission";O:25:"Statamic\Forms\Submission":4:{ s:29:"Statamic\Forms\Submissionid";N; s:4:"form";O:19:"Statamic\Forms\Form":7:{ s:9:"*handle";s:10:"newsletter"; s:8:"*title";s:10:"Newsletter"; s:12:"*blueprint";N; s:11:"*honeypot";N; s:8:"*store";N; s:8:"*email";N; s:10:"*metrics";N; } s:7:"*data";a:3:{ s:4:"name";s:4:"Alex"; s:6:"email";s:15:""; } s:14:"*supplements";O:29:"Illuminate\Support\Collection":2:{ s:8:"*items";a:0:{} s:28:"*escapeWhenCastingToString";b:0; } } s:4:"form";r:2; }
Logged information from the event object, formatted for readability

That's a lot of input at once, but for our case all we need to know is that the event object has a submission property which contains the information about both the form and the data submitted by the user.

Sending the Signup to an API

We have located our data, so now we can pick out the information we need to create the API call and sign the user up for our newsletter.

As of now our function is executed for every submission of any form on the website, which would probably cause errors once we also create other forms. But since our submission data includes the form object, we can simply check for the correct handle and filter out unwanted submissions.

/app/listeners/NewsletterFormListener.php (excerpt)
public function handle(FormSubmitted $event) { if ($event->submission->form->handle() == 'newsletter') { // the API call goes here } }
Making sure we only handle submissions for the newsletter form

Note that we are using the getter-method handle(), since the handle property is protected and cannot be directly accessed from outside the class. This value is simply a unique name of our form and has no connection to handling the form submission.

As for the submission data we can actually use a bit of Laravel's magic. If you call $event->submission->name, the app knows to use the get() method in the background to retrieve the value.

Since the exact API call can take many forms, we will just do a overly simplified one here that sends our two data points to a fictional endpoint. In the real world you would probably have to take care of authorization first and send some additional information about your mailing list along.

/app/listeners/NewsletterFormListener.php (excerpt)
public function handle(FormSubmitted $event) { if ($event->submission->form->handle() == 'newsletter') { $client = new GuzzleHttp\Client(); $result = $client->request('POST', 'https:/', [ 'form_params' => [ 'name' => $event->submission->name, 'email' => $event->submission->email, ] ]); if ($result->getStatusCode() === 200) { return false; } else { throw \Illuminate\Validation\ValidationException::withMessages(['API call failed']); } } }
Sending the form submission data to a fictional API endpoint

When anything goes wrong with the API call we can throw a ValidationException and display an error message on the frontend. Otherwise we can return a boolean to let Statamic know that our custom form handling worked as planned.

Returning true lets the app know that you would like to also store this submission in the backend or send it out via e-mail, setting it to false will prevent that. Since we have no use for the data after sending it to the external API, we can simply return false and be done with it.

Final Thoughts

This is a very simple way of handling the user input, but since you can access all information this way, there is no limit as to what functionality you can implement.

This approach has its limits however, since you need to hard code the form handle and API call into you listener. For more flexibility you might want to turn this into a package instead and add a config file where the site owner can change those values as needed.

More posts: