Prevent spam in Statamic forms
Tags: statamic, laravel, php, tutorial
Published: 2023-11-13 21:33:38

If you're using [Statamic's Forms feature](https://statamic.dev/forms) to power a public facing form, you'll benefit from using some sort of spam prevention service. I've built plenty of "Contact Us" forms in my day, and I can tell you that eventually it will be targeted with spam messages. It's just a matter of time.

In this article, I'll show you how to add a spam detection service to your Statamic forms and reject any submissions that are deemed to be spam. I'll be using [Akismet](https://akismet.com/) for this example, but you can use any spam detection service you'd like.

The first step is to create an account with Akismet (or similar service), get your API key, and add it to your .env file:

```bash
AKISMET_API_KEY="your-api-key"
```

...and add a new service to `config/services.php` so that we can access the API key in our code:

```php
'akismet' => [
    'key' => env('AKISMET_API_KEY'),
],
```

Next, we'll create a listener that will be triggered when a form is submitted. Statamic dispatches a [`FormSubmitted` event](https://statamic.dev/extending/events#formsubmitted) when a form is submitted, but before the submission is saved. The docs say:

> You can return false to prevent the submission, but appear to the user as though it succeeded.

This will be perfect for our use case. We can return `false` in the listener if the submission is deemed to be spam, resulting in the submission not being saved and the site owner not being notified of a new submission.

Create the listener with the following command:

```bash
php artisan make:listener HandleFormSubmitted
```

...and don't forget to register it in `app/Providers/EventServiceProvider.php`:

```php
<?php

namespace App\Providers;

use Statamic\Events\FormSubmitted;
use App\Listeners\HandleFormSubmitted;
use Illuminate\Foundation\Support\Providers\EventServiceProvider as ServiceProvider;

// use Illuminate\Support\Facades\Event;

class EventServiceProvider extends ServiceProvider
{
    /**
     * The event to listener mappings for the application.
     *
     * @var array<class-string, array<int, class-string>>
     */
    protected $listen = [
        FormSubmitted::class => [
            HandleFormSubmitted::class,
        ],
    ];

    // ...
}
```

Now, we can work on the listener. It's important to note that this code will run when _any_ form is submitted, so we'll need to filter out the form submissions that we don't want to check for spam.

The listener's `handle` method will receive a `FormSubmitted` event, which contains the form being submitted and the submission itself. We can use this information to determine if we want to check for spam.

In this example, we'll only be checking the form with the handle of `contact_us` but if you have multiple forms that you want to check for spam, you could create an array of form handles and check if the submitted form is in that array.

```php
<?php

namespace App\Listeners;

use Statamic\Events\FormSubmitted;

class HandleFormSubmitted
{
    public function handle(FormSubmitted $event): bool
    {
        // Form submissions that aren't for the contact_us form are saved as normal.
        if ($event->submission->form()->handle() !== 'contact_us') {
            return true;
        }

        return ! $this->isSpam($event);
    }

    protected function isSpam(FormSubmitted $event): bool
    {
        // Return true if the submission is spam.
    }
}
```

You should implement the `isSpam` method according to the spam detection service you're using. For Akismet, I went with something like this:

```php
use Illuminate\Support\Facades\Http;

protected function isSpam(FormSubmitted $event): bool
{
    $formData = $event->submission->data();

    $response = Http::asForm()
        ->withUserAgent(config('app.name'))
        ->post('https://rest.akismet.com/1.1/comment-check', [
            'api_key' => config('services.akismet.key'),
            'blog' => config('app.url'),
            'user_ip' => request()->ip(),
            'user_agent' => request()->userAgent(),
            'referrer' => request()->headers->get('referer'),
            'comment_type' => 'contact-form',
            'comment_author' => $formData->get('name'),
            'comment_author_email' => $formData->get('email'),
            'comment_content' => $formData->get('message'),
            'comment_date_gmt' => now()->toIso8601String(),
            'blog_lang' => 'en',
            'blog_charset' => 'UTF-8',
            'user_role' => 'guest',
            'is_test' => ! app()->environment('production'),
        ]);

    return $response->body() === 'true';
}
```

I've removed some logging and error handling for brevity.

That about does it, your Statamic forms should now be protected from spam submissions. This is a good starting point but there's plenty of room for code and feature improvements, such as:

- Showing spam submissions in the Statamic Control Panel
- Submitting false positives/ham from the Statamic Control Panel
- Being able to swap out the spam detection service for a different one
- etc.

Maybe I'll write a follow up article on implementing some of these features, please let me know if that's something you'd be interested in.

I hope you found this article useful. If you have any questions or feedback, let's chat in the comments below. Or, you can always reach me on [Bluesky](https://bsky.app/profile/stephensamra.com) or [Twitter](https://twitter.com/stephensamra_).