Unobtrusive spam-protection with PHP

If you are like me and absolutely hate solving weird visual captcha's with sometimes really impossible questions (is that small corner part of the bike/zebra crossing/bridge or not?) then you must've wondered about how to prevent forms (such as contact forms or comment forms) against spam. Of course, a 100% protection does not exist, but it would be good to have at least some protection.

When I recently updated the Ingewikkeld website with a contact form I quickly got swamped with spam messages. The first thought for many people will be "just add a CAPTCHA". But I do not want to scare away potential customers. I want to make it as easy as possible for them to contact me.

So I set out to search for an unobtrusive alternative. After searching for a bit and not really finding something, I saw the name Cloudflare. Now, I know them, we use their DNS services as well. But I had not yet heard of their Cloudflare Turnstile product. I checked it out and it seemed to do exactly what I wanted. A check, without required interaction. Nice!

I configured it for the ingewikkeld.dev domain and opened my IDE to implement it. There is great documentation and the frontend part was implemented in a matter of seconds. But Turnstile also relies on a backend part: The frontend does a call to Turnstile to obtain a token and "clear" the user. The backend then has to verify that token to confirm that Turnstile indeed cleared the user. It's a relatively simple HTTP request that I could build myself using an HTTP client class, but I decided to check https://packagist.org to see if there was a library that already implemented this functionality. It turns out there are already quite a few.

After a bit of checking I decided to try and implement usarise/turnstile. It seemed simple, supports Symfony's PSR-18 HTTP client out of the box and since the Ingewikkeld website is built with Symfony, that's good. And indeed, it was exactly as easy as I had hoped. Make sure the Turnstile\Turnstile class is injectable via the container, and the actual logic is easy:

$turnstileResponse = $turnstile->verify($request->request->get('cf-turnstile-response'));

if ($turnstileResponse->success === true) {
  // logic
} else {
  // set a flash message and redirect back to the form
}

It's as easy as that! Why would anyone still use a CAPTCHA if this is also an option?