We wrote an article long time ago, about how to implement the Google ReCaptcha in your Symfony 3 project, which works fine as long as you can rely on the mentioned tool. However, some applications restrict the usage of Google products. So, the developer will have to implement another option to prevent bots from filling forms randomly on the web. One of the most known libraries to achieves this easily in PHP is with the BotDetect captcha library.
In this article, we will explain you how to install and use the BotDetect Captcha library in Symfony 4.
1. Install BotDetect Captcha
BotDetect CAPTCHA generator is a non-stalking form-security solution that uses a mix of measures, that are easy for humans but hard for bots, to prevent automated form posting. BotDetect also provides an audio Captcha alternative to keep websites accessible to people with impaired vision, enabling you to make WCAG and Section 508 compliant websites.
In Symfony 4, you can easily install the library through composer using the symfony-captcha-bundle with the following command:
composer require captcha-com/symfony-captcha-bundle:"4.*"
Note: If you are using Symfony 5, the official package doesn't support for this version yet. However @carlos-mg89 decided to fork the project and create a Symfony 5 compatible package that you can find in the official repository at Github here, so instead of following this tutorial, you will need to follow an extra step mentioned in the documentation of the project here.
This will install the library and will register its classes automatically on your project. For more information about this library, please visit the official website here.
2. Register Captcha Bundle Routes
In order to render the captcha, request audios etc. you will need to register the routes that the bundle already includes in the /config/routes.yaml
file like this:
# /app/config/routes.yaml
captcha_routing:
resource: "@CaptchaBundle/Resources/config/routing.yml"
This will allow the captcha to work in the frontend once it has been implemented in the backend.
3. Create configuration file
Now you will need to create a configuration file in PHP in the config/packages directory of your application. I know, we usually add only yaml files in this directory, however this library is a special case as it has a hardcoded require that includes a PHP file from the mentioned directory, so you will need to add obligatorily a PHP file with a simple if and a return with an array:
<?php
// app/config/packages/captcha.php
if (!class_exists('CaptchaConfiguration')) { return; }
// BotDetect PHP Captcha configuration options
return [
// Captcha configuration for example form
'ExampleCaptchaUserRegistration' => [
'UserInputID' => 'captchaCode',
'ImageWidth' => 250,
'ImageHeight' => 50,
],
];
The returned associative array will have as keys the name of the custom configuration of a single field. In this example, we will have only a single captcha configuration that will be used for an User registration form. The idea is to customize every captcha wherever you need it, you can specify which configuration will be used later on the FormType.
4. Add a CaptchaType field to your FormType
Following the Symfony guidelines, when we create a form we should obviously have a FormType associated to some Entity. In this case, as we mentioned previously, we will have a registration form that allows the user to register a new user in the database, this means that the form will persist an User entity in the database. The form contains the regular fields as first name, last name, email and password. However, it should contain as well a new field namely the captcha code, this can be simply added just like the other fields in the buildForm method of the Type.
Note that the field, although isn't on the database, it should be added as a mapped field, just like it looks in the following example:
<?php
// app/src/Form/RegistrationFormType.php
namespace App\Form;
// ... ///
// Import the Captcha Field Type and Validator
use Captcha\Bundle\CaptchaBundle\Form\Type\CaptchaType;
use Captcha\Bundle\CaptchaBundle\Validator\Constraints\ValidCaptcha;
// ... ///
class RegistrationFormType extends AbstractType
{
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder
// ... ///
->add('captchaCode', CaptchaType::class, array(
'captchaConfig' => 'ExampleCaptchaUserRegistration',
'constraints' => [
new ValidCaptcha([
'message' => 'Invalid captcha, please try again',
]),
],
))
// ... ///
;
}
// ... ///
}
Theorically, you would be asking to yourself now, that this would make your form fail as we are registering a new field that isn't associated with the User entity, and you're right ! But don't run your app yet, follow the last step and you'll understand why.
5. Add the captchaCode field to your entity
Finally, you will just need to add a new "field" to your entity, that won't be mapped of course, is just need to exist in the entity that you will be trying to persist into the database, so the library can check if the captcha provided by the user was ok or not:
<?php
// src/Entity/User.php
namespace App\Entity;
use Doctrine\ORM\Mapping as ORM;
class User
{
// ... //
protected $captchaCode;
public function getCaptchaCode()
{
return $this->captchaCode;
}
public function setCaptchaCode($captchaCode)
{
$this->captchaCode = $captchaCode;
}
// ... //
}
With this, you should be able now to run your application and you will see the captcha. Don't forget to render the new field in your form view:
{% extends 'base.html.twig' %}
{% block title %}Register{% endblock %}
{% block body %}
<h1>Register</h1>
{{ form_start(registrationForm) }}
{{ form_row(registrationForm.firstName) }}
{{ form_row(registrationForm.lastName) }}
{{ form_row(registrationForm.email) }}
{{ form_row(registrationForm.plainPassword) }}
{{ form_row(registrationForm.agreeTerms) }}
{# render the captcha field to the form in the view #}
{{ form_row(registrationForm.captchaCode) }}
<button class="btn">Register</button>
{{ form_end(registrationForm) }}
{% endblock %}
How to solve incompatibility issue: Unexpected "spaceless" tag
A couple of users reported that in the latest versions of Symfony, where Twig 2.7 is used, the following exception appears:
This happens because Twig decided to drop support for the spaceless block, converting it into a filter. The captcha bundle has been unmaintained for a long time now and even though a developer @sumuhire decided to create a pull request to solve the issue, it hasn't been merged yet. To solve this problem without modifying anything in the vendor directory of your project, you need to override the default captcha template of the bundle. Create the the following structure of directories and captcha.html.twig file inside your project's template directory (/your-app/templates/bundles):
templates
└── bundles/
└── CaptchaBundle/
└── captcha.html.twig
The content of the captcha.html.twig file will be the following one:
{#
sf4application/templates/bundles/CaptchaBundle/captcha.html.twig
https://github.com/captcha-com/symfony-captcha-bundle/pull/11/commits/a7d36269a192be90e71df28669238a3f7c50a550
#}
{% block simple_captcha_widget %}
{% apply spaceless %}
{{ captcha_html | raw }}
{{ form_widget(form, { 'id': user_input_id, 'value': '' }) }}
{% endapply %}
{% endblock %}
{% block captcha_widget %}
{% apply spaceless %}
{{ captcha_html | raw }}
{{ form_widget(form, { 'id': user_input_id, 'value': '' }) }}
{% endapply %}
{% endblock %}
Save the changes and clear your applications cache:
php bin/console cache:clear
Try to access your application once again and the exception won't appear anymore. This will basically replace the default template of the library, preventing that tedious exception from appearing.
How to hide "BotDetect CAPTCHA Library for Symfony" message
Pitifully, the library doesn't include a way to hide that message as its hardcoded in the library, but you shouldn't modify vendor classes. A workaround to remove this is through JavaScript, once the DOM has loaded:
$(function(){
$("a[title ~= 'BotDetect']").removeAttr("style");
$("a[title ~= 'BotDetect']").removeAttr("href");
$("a[title ~= 'BotDetect']").css('visibility', 'hidden');
});
Happy coding !