tbaronnat/icon-recaptcha-bundle
A Symfony bundle that integrates Google reCAPTCHA into forms, with configuration for API keys, a reusable form type, and server-side validation to help protect applications from spam and automated submissions.
First, calibrate expectations: tbaronnat/icon-recaptcha-bundle is a Symfony bundle, not a Laravel package. It ships a Bundle class, DependencyInjection/Configuration.php, and Resources/config/services.* — none of which Laravel autoloads. As a Laravel dev you are evaluating it either because you maintain a Symfony app alongside Laravel, or because you want to borrow the integration pattern.
Where to look first, in this order:
composer.json — confirm type: symfony-bundle, the required Symfony version constraint, and the last commit date. A 0-star bundle with no recent activity is a reference implementation, not a dependency.DependencyInjection/Configuration.php — this is the actual contract: which config keys (public_key, private_key, theme, size, lang, etc.) the bundle accepts and whether they map to env vars.Form/Type/ and Validator/Constraints/ — the two integration surfaces. Everything else is glue.Resources/views/ — the Twig snippet that renders the widget and injects the Google script tag.Minimal Symfony install (if you actually are on Symfony):
composer require tbaronnat/icon-recaptcha-bundle
// config/bundles.php
Icon\ReCaptchaBundle\IconReCaptchaBundle::class => ['all' => true],
# config/packages/icon_recaptcha.yaml
icon_recaptcha:
public_key: '%env(RECAPTCHA_PUBLIC_KEY)%'
private_key: '%env(RECAPTCHA_PRIVATE_KEY)%'
// Form
$builder->add('captcha', ReCaptchaType::class);
First use case: a contact or registration form. Register your Google keys, add the type to a form, submit, and confirm the server-side siteverify call succeeds — that round trip is the whole value of the package.
If you are on Laravel, do not install this. You will get an autoloaded Bundle class that never boots, a Symfony Configuration tree Laravel ignores, and zero ServiceProvider. Treat the repo purely as documentation for the reCAPTCHA flow and implement the ~30 lines yourself (see next section).
Form-type composition (Symfony). The idiomatic use is adding the type to an existing form builder rather than calling the API directly. The type renders the widget and its constraint fires on POST:
$form = $this->createFormBuilder($contact)
->add('name', TextType::class)
->add('email', EmailType::class)
->add('captcha', ReCaptchaType::class) // adds widget + constraint
->add('submit', SubmitType::class)
->getForm();
Standalone constraint. If you have a non-form entry point (JSON API, Livewire-style component, artisan-invoked job), instantiate the constraint and validate the token yourself instead of dragging in a Symfony form.
Config-first key handling. Always bind public/private keys through env vars, never hardcode. Distinguish the two: the public key is safe in the rendered page and cacheable; the private key must never leave the server and must be excluded from any cache-busting build step.
Laravel port — the pattern you actually want. The bundle's server-side logic is a single POST to Google. Reimplement it as a validation rule, which fits Laravel's day-to-day far better:
// app/Rules/Recaptcha.php
public function passes($attribute, $value)
{
$res = Http::asForm()->timeout(5)->post(
'https://www.google.com/recaptcha/api/siteverify',
[
'secret' => config('services.recaptcha.secret'),
'response' => $value,
'remoteip' => request()->ip(),
]
);
if (! $res->ok() || ! $res->json('success')) {
return false;
}
// v3 only: enforce score + action
return ($res->json('score') ?? 1.0) >= 0.5
&& ($res->json('action') ?? 'submit') === 'submit';
}
Use it as 'g-recaptcha-response' => ['required', new Recaptcha], and render the widget in Blade with @push('scripts') so the Google <script> loads once per page.
Verification of v3 vs v2. Decide early. v2 (checkbox/invisible) is pass/fail — a single boolean. v3 returns a score 0.0–1.0 and an action string; you must check both, and the action check is what prevents a token harvested on /login being replayed on /checkout.
Integration into an existing pipeline. For AJAX/SPA flows, keep the widget mounted and regenerate tokens on demand (grecaptcha.execute for v3, grecaptcha.reset for v2) — tokens are single-use, so a failed validation must reset before the user retries.
composer.json; you get dead weight and a class that never boots.grecaptcha.reset() in the error path.remoteip is optional but useful. Passing the client IP helps Google flag abuse; note it becomes wrong behind a proxy unless TrustProxies / setTrustedProxies is configured.success only, which means a score of 0.1 still passes. Always enforce a threshold (0.5 is Google's default suggestion) and match action.siteverify synchronously. Wrap the call in a short timeout (Google occasionally lags) and degrade gracefully — a 5s hang on your contact form is worse than a soft-fail.https://www.google.com/recaptcha/, https://www.gstatic.com/recaptcha/, and a frame-src for https://www.google.com. Missing directives produce a silently blank widget that looks like a backend bug.Configuration will complain if public_key/private_key are missing, but it won't validate that they're the right version (v2 vs v3) — a mismatched key gives invalid-input-secret, not a helpful config error.siteverify via a service override — otherwise you're editing the bundle. That's another reason to own the 30-line implementation yourself.siteverify body — error-codes (missing-input-response, timeout-or-duplicate, invalid-input-secret) tells you precisely which layer broke, and it's the fastest way to distinguish a frontend rendering failure from a key or network problem.How can I help you explore Laravel packages today?