On one of the sites I have set up with a contact form there started to get quite a few automated spam messages.

To avoid this I could add something like reCaptcha but first I'll try to take care of this on my own and not add more Google stuff to the page.

My form was pretty simple :

public function buildForm(FormBuilderInterface $builder, array $options)
    {
        $builder
            ->add('name', TextType::class,
                [
                'required' => false,
                 'label' => 'form.contact.name',
                  'translation_domain' => 'Default'
                  ]
            )
            ->add('email', EmailType::class,
                [
                    'required' => false,
                    'label' => 'form.contact.email',
                    'translation_domain' => 'Default',
                ]
            )
            ->add('url', UrlType::class,
                [
                'required' => false,
                 'label' => 'form.contact.url',
                  'translation_domain' => 'Default'
                  ]
            )
            ->add('message', TextType::class,
                [
                'required' => false,
                 'label' => 'form.contact.message',
                  'translation_domain' => 'Default'
                  ]
            );
    }

Based on my Message Entity :

/**
 * @ORM\Entity(repositoryClass="App\Repository\MessageRepository")
 */
class Message
{
    use TimestampableEntity;

    private $id;
    private $name;

    /**
     * @ORM\Column(type="string", length=255)
     * @Assert\NotBlank
     * @Assert\Email(
     *     message = "The email '{{ value }}' is not a valid email.",
     *       mode="strict"
     * )
     */
    private $email;

    private $url;

    private $message;
 // ...
}

The form gets send with an Ajax request and then gets treated in the Controller :

/**
 * @Route("message", name="send_message", options={"expose"=true}, methods={"POST"})
 * @param Request $request
 * @param TranslatorInterface $translator
 * @return JsonResponse
 */
public function sendMessage(Request $request, TranslatorInterface $translator): JsonResponse
{
    $messageForm = $this->createForm(MessageType::class, null);
    $messageForm->handleRequest($request);

    if ($messageForm->isSubmitted() && $messageForm->isValid()) {
        /** @var Message $message */
        $message = $messageForm->getData();

        $this->manager->persist($message);

        $this->manager->flush();

        return new JsonResponse($translator->trans('form.contact.sent', [], 'Default'));
    } else {
        $errors = $this->getErrorsFromForm($messageForm);
        return new JsonResponse($errors, Response::HTTP_BAD_REQUEST);
    }
}

Great so now let's add some stuff to deceive bots !

First off I imagine they recognize a field called email and are eager to fill it in. So I want to keep an email field but also a weird random field that will contain the actual email if someone fills in the form correctly.

So I added a property to my Message entity with the email Assertions, and I removed them from the email field :

class Message
{

    public const EMAIL_HIDDEN_INPUT = 'fhjgiz46';

    //...

    /**
     * @ORM\Column(type="string", length=255)
     */
    private $email;

    /**
     * @Assert\NotBlank
     * @Assert\Email(
     *     message = "The email '{{ value }}' is not a valid email.",
     *       mode="strict"
     * )
     */
    private $fhjgiz46;

    //...
}

This new field is not mapped and will therefore not appear in the database. Next I added it to my FormType :

public function buildForm(FormBuilderInterface $builder, array $options)
{
    $builder
//...
        ->add('email', EmailType::class, 
            [
                'required' => false,
                'attr' => ['class' => 'email']
            ]
        )
        ->add(Message::EMAIL_HIDDEN_INPUT, EmailType::class,
            [
                'required' => false,
                'label' => 'form.contact.email',
                'translation_domain' => 'Default',
            ]
        );
//...
}

I added a class to my honey-pot input to make it invisible to the user with some CSS rules :

input.email {
    height: 0;
    margin: 0;
    text-decoration: none;
    border: none;
}

Finally, in the controller I will want to check if the email input is set, if so this means it was a bot and therefore I do not want to send an email. If this is not set, but the new randomly named parameter is set I want to update the actual email to that value :

public function sendMessage(Request $request, TranslatorInterface $translator): JsonResponse
    {
        $messageForm = $this->createForm(MessageType::class, null);
        $messageForm->handleRequest($request);
        if ($messageForm->isSubmitted() && $messageForm->isValid()) {

            /** @var Message $message */
            $message = $messageForm->getData();

            //Check honeypot
            if ($message->getEmail()) {
                $this->logger->alert(
                        sprintf('Spam tried with %s from %s', $message->getEmail(), $request->getClientIp())
                    );
                return new JsonResponse(
                        $translator->trans('form.contact.error', [], 'Default'), Response::HTTP_NOT_ACCEPTABLE
                    );
            }

            //If honeypot not set swap the hidden email input
            if ($message->getFhjgiz46()) {
                $message->setEmail($message->getFhjgiz46());
            }

            $this->manager->persist($message);

            $this->manager->flush();

            return new JsonResponse($translator->trans('form.contact.sent', [], 'Default'));
        } else {
            $errors = $this->getErrorsFromForm($messageForm);
            return new JsonResponse($errors, Response::HTTP_BAD_REQUEST);
        }
    }

To keep an eye on how well this would work I added a Logger to log an alert every time the honey-pot is triggered and, what do you know, I got spammed :

cat var/log/prod-2021-03-19.log  | grep ALERT
[2021-03-19T13:17:31.314259+01:00] app.ALERT: Spam tried with ashleegravell@aol.com from 212.116.86.206 [] []
[2021-03-19T13:25:27.326076+01:00] app.ALERT: Spam tried with noahreyna2018@icloud.com from 213.230.70.146 [] []
[2021-03-19T13:49:27.272809+01:00] app.ALERT: Spam tried with webcowboy@verizon.net from 103.27.207.36 [] []
[2021-03-19T14:21:14.832344+01:00] app.ALERT: Spam tried with speedee44@aol.com from 63.237.228.123 [] []
[2021-03-19T14:43:25.047792+01:00] app.ALERT: Spam tried with jane_eisen2007@yahoo.com from 196.46.199.5 [] []

If they keep on trying with the same IP address I will add some Fail2Ban configuration as well.