Symfony CAPTCHA Integration
This recipe shows how to integrate TrustCaptcha into a Symfony application. The frontend setup is the same as for any other PHP application — this page focuses on the server-side validation.
The setup section gets you to a working integration in three small steps using a controller method directly. Below it, an optional refactor section shows the more reusable Symfony-idiomatic approach (custom Validator constraint).
Preparation
Section titled “Preparation”You should have already completed the following steps before you wire TrustCaptcha into your Symfony application.
Read Get-Started: Get a quick overview of the concepts behind TrustCaptcha and the integration process in get started.
Existing CAPTCHA: If you don’t have a CAPTCHA yet, sign in or create a new user account. Then create a new CAPTCHA.
1. Embed the frontend widget
Section titled “1. Embed the frontend widget”First, add the TrustCaptcha script to your page (see the JavaScript Guide for version pinning and self-hosting options).
Then place the <trustcaptcha-component> element inside your Twig form. The widget appends a hidden tc-verification-token field on submit, which your Symfony controller receives like any other form input.
{% raw %}<script type="module" src="https://cdn.trustcomponent.com/trustcaptcha/3.0.x/trustcaptcha.esm.min.js"></script>
<form method="post" action="{{ path('contact_submit') }}"> <input type="hidden" name="_token" value="{{ csrf_token('contact') }}">
<label>Email</label> <input type="email" name="email" required>
<trustcaptcha-component sitekey="<your_site_key>"></trustcaptcha-component>
<button type="submit">Send</button></form>{% endraw %}See the Widget Overview for the full property reference.
2. Install the PHP SDK
Section titled “2. Install the PHP SDK”composer require trustcomponent/trustcaptcha-php:^3.03. Validate the token in your controller
Section titled “3. Validate the token in your controller”<?php
namespace App\Controller;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;use Symfony\Component\HttpFoundation\Request;use Symfony\Component\HttpFoundation\Response;use Symfony\Component\Routing\Attribute\Route;use TrustComponent\TrustCaptcha\TrustCaptcha;
class ContactController extends AbstractController{ #[Route('/contact', name: 'contact_submit', methods: ['POST'])] public function submit(Request $request): Response { $apiKey = '<your_api_key>'; // In production, load from env: $_ENV['TRUSTCAPTCHA_API_KEY'] $token = $request->request->get('tc-verification-token', '');
try { $trustCaptcha = new TrustCaptcha($apiKey); $result = $trustCaptcha->getVerificationResult($token); } catch (\Throwable $e) { return $this->render('contact/index.html.twig', ['error' => 'CAPTCHA verification failed.']); }
if (!$result->verificationPassed || $result->score > 0.5) { return $this->render('contact/index.html.twig', ['error' => 'CAPTCHA verification failed.']); }
// CAPTCHA passed — request data is safe to use. // ... your business logic ...
return $this->redirectToRoute('contact_success'); }}That’s it — the form is now protected. For real deployments, move the API key out of the source code (see the comment) and consider explicit failover handling — see Failover Behavior for the reasoning and a code template.
Refactor: extract to a custom constraint
Section titled “Refactor: extract to a custom constraint”If you protect more than one route — or use Symfony’s Form component — the most idiomatic approach is a custom Bean Validation constraint. The verification call then runs automatically as part of Symfony’s normal validation pipeline.
Configure the API key
Section titled “Configure the API key”TRUSTCAPTCHA_API_KEY=<your_api_key>parameters: trustcaptcha.api_key: '%env(TRUSTCAPTCHA_API_KEY)%'
services: App\Validator\TrustCaptchaTokenValidator: arguments: $apiKey: '%trustcaptcha.api_key%' tags: [validator.constraint_validator]Create the constraint and validator
Section titled “Create the constraint and validator”<?php
namespace App\Validator;
use Symfony\Component\Validator\Constraint;
#[\Attribute(\Attribute::TARGET_PROPERTY | \Attribute::TARGET_PARAMETER)]class TrustCaptchaToken extends Constraint{ public string $message = 'CAPTCHA verification failed.';}<?php
namespace App\Validator;
use Symfony\Component\Validator\Constraint;use Symfony\Component\Validator\ConstraintValidator;use TrustComponent\TrustCaptcha\TrustCaptcha;
class TrustCaptchaTokenValidator extends ConstraintValidator{ public function __construct(private readonly string $apiKey) {}
public function validate(mixed $value, Constraint $constraint): void { if (!is_string($value) || $value === '') { $this->context->buildViolation($constraint->message)->addViolation(); return; }
try { $trustCaptcha = new TrustCaptcha($this->apiKey); $result = $trustCaptcha->getVerificationResult($value);
if (!$result->verificationPassed || $result->score > 0.5) { $this->context->buildViolation($constraint->message)->addViolation(); } } catch (\Throwable $e) { $this->context->buildViolation($constraint->message)->addViolation(); } }}Use the constraint in your controller
Section titled “Use the constraint in your controller”Inject the ValidatorInterface, pull the token directly out of the request, and run the constraint against it. This works regardless of whether the rest of your form goes through the Symfony Form component:
use App\Validator\TrustCaptchaToken;use Symfony\Component\Validator\Validator\ValidatorInterface;
public function submit(Request $request, ValidatorInterface $validator): Response{ $token = $request->request->get('tc-verification-token', '');
$violations = $validator->validate($token, new TrustCaptchaToken()); if (count($violations) > 0) { return $this->render('contact/index.html.twig', ['error' => 'CAPTCHA verification failed.']); }
// CAPTCHA passed — request data is safe to use. return $this->redirectToRoute('contact_success');}The constraint becomes a one-liner you can drop into any controller — no controller logic to copy/paste, all error handling lives inside TrustCaptchaTokenValidator.
CSRF. Symfony’s CSRF protection and TrustCaptcha are independent layers — both should stay enabled. The CAPTCHA token does not replace the CSRF token.
Field name. The default token field name contains dashes (tc-verification-token). $request->request->get('tc-verification-token') reads it without further setup. If you’d rather use camelCase, set token-field-name="trustcaptchaToken" on the widget and adapt the controller code.
Configured SDK options. For custom timeouts, proxy, or a custom API host, pass them as the second constructor argument: new TrustCaptcha($apiKey, ['apiHost' => ..., 'proxy' => ...]). See the PHP Guide for the full constructor options.
Next steps
Section titled “Next steps”Once you have wired TrustCaptcha into your Symfony application, you can use TrustCaptcha to its full extent. However, we still recommend the following additional technical and organizational measures:
Security rules: You can find many security settings for your CAPTCHA in the CAPTCHA settings. These include, for example, authorized websites, CAPTCHA bypass for specific IP addresses, bypass keys, IP based blocking, geoblocking, individual difficulty and duration of the CAPTCHA, and much more. Learn more about the security rules.
Privacy & GDPR compliance: Include a passage in your privacy policy that refers to the use of TrustCaptcha. We also recommend that you enter into a data processing agreement with us to stay GDPR-compliant. Learn more about data protection.
Accessibility & UX: Customize TrustCaptcha to your website so that your website is as accessible as possible and offers the best possible user experience. More about accessibility.
Failover behavior: Decide how your backend should behave when our service is temporarily unreachable. This is particularly important for high-availability flows where blocking real users during an outage is worse than letting through a small amount of unverified traffic. Learn more about failover behavior.
Testing: If you use automated testing, make sure that the CAPTCHA does not block it. Learn more about testing.