Weave Code
Weaver
Helps Laravel developers discover, compare, and choose open-source packages. See popularity, security, maintainers, and scores at a glance to make better decisions.
Feedback
Share your thoughts, report bugs, or suggest improvements.
Subject
Message

Recaptcha Bundle Laravel Package

gjuric/recaptcha-bundle

gjuric/recaptcha-bundle integrates Google reCAPTCHA into Symfony forms with a dedicated form type, server-side validation, and configurable keys. It supports reCAPTCHA v2 and v3, handles verification responses, and lets you protect forms and endpoints from spam and bots with minimal configuration.

View on GitHub
Deep Wiki
Context7

Getting Started

What changed in v1.6.2: one line — "Fixes Symfony 8.0 deprecations." No new features, no behavioral changes, no config-key changes. It is a compatibility maintenance cut, which is useful information (the fork is alive and tracking Symfony 8) but changes nothing about day-to-day usage, and nothing about the Laravel story below.

First, the honest framing: this is a Symfony bundle, not a Laravel package. There is no service provider, no facade, no config/recaptcha.php. There is still no illuminate/* dependency in v1.6.2. It hooks into symfony/form, symfony/http-kernel, Twig form themes, and the DI container. From a Laravel codebase you cannot composer require it and get value — you can read it, port ~80 lines of it, or copy the widget markup. Treat the assessment below as "what this bundle does, and what I'd rebuild in Laravel."

Minimal Symfony steps (if you're in the target framework):

composer require gjuric/recaptcha-bundle
// config/bundles.php
EWZ\Bundle\RecaptchaBundle\EWZRecaptchaBundle::class => ['all' => true],
# config/packages/ewz_recaptcha.yaml
ewz_recaptcha:
    public_key:  '%env(EWZ_RECAPTCHA_PUBLIC_KEY)%'
    private_key: '%env(EWZ_RECAPTCHA_PRIVATE_KEY)%'
    enabled:     true

Where to look first, in this order:

  1. README.md — the options table is the real documentation; the API surface is tiny.
  2. DependencyInjection/Configuration.php — the authoritative list of config keys and defaults. Don't trust blog posts; forks drift.
  3. composer.json — v1.6.2's whole point is a Symfony 8 constraint widening. Read the require block before adopting: Symfony 8 means PHP 8.4, so if you're pinned to Symfony 7 / PHP 8.2–8.3 you likely need the previous tag, not this one.
  4. Form/Type/RecaptchaType.php — what options the field accepts (ajax, hide_badge, locale, attr...).
  5. Validator/Constraints/IsTrue.php (+ its validator) — the actual server-side verify call. This is the file you'd port.
  6. Resources/views/Form/*.html.twig — the widget block; you'll override this constantly for Bootstrap/Tailwind.
  7. Service/ReCaptcha*.php — the HTTP client against siteverify; check proxy/timeout handling.

First use case — a contact form:

use EWZ\Bundle\RecaptchaBundle\Form\Type\RecaptchaType;
use EWZ\Bundle\RecaptchaBundle\Validator\Constraints\IsTrue as RecaptchaTrue;

$builder
    ->add('name')
    ->add('email')
    ->add('message', TextareaType::class)
    ->add('recaptcha', RecaptchaType::class, [
        'mapped'      => false,
        'constraints' => [new RecaptchaTrue()],
        'attr'        => ['options' => ['theme' => 'light']],
    ]);
{{ form_start(form) }}
    {{ form_widget(form) }}
    <button>Send</button>
{{ form_end(form) }}

That's it — the widget block emits the g-recaptcha div and the Google script tag. If you get Block "recaptcha_widget" does not exist, register the bundled theme (see Gotchas).

Laravel translation of the same thing (what you'd actually ship — unchanged by this release):

// app/Rules/Recaptcha.php
public function passes($attribute, $value): bool
{
    if (! config('services.recaptcha.enabled')) {
        return true; // mirrors the bundle's `enabled: false`
    }

    $r = Http::asForm()->timeout(5)->post('https://www.google.com/recaptcha/api/siteverify', [
        'secret'   => config('services.recaptcha.secret'),
        'response' => $value,
        'remoteip' => request()->ip(),
    ]);

    if (! $r->ok() || ! $r->json('success')) {
        return false;
    }

    // v3 only: score gate
    $threshold = config('services.recaptcha.score', 0.5);

    return $r->json('score', 1.0) >= $threshold;
}

Plus a Blade partial that renders <div class="g-recaptcha" data-sitekey="..."> and @once @push('scripts') <script src="https://www.google.com/recaptcha/api.js" async defer></script> @endpush @endonce. That is 95% of what the bundle gives you, minus the Symfony plumbing.

Implementation Patterns

Nothing in this section changed in v1.6.2. Deprecation fixes are internal (attribute/return-type/injection-style churn against Symfony 8 internals); the form type options, config keys, and Twig blocks are the same. Existing patterns below are still the whole playbook.

Invisible vs. checkbox vs. score (v2 vs v3). The bundle supports both, but the mental model differs: v2 returns a boolean you assert with IsTrue; v3 returns a score and needs a score_threshold in config plus an action you should be asserting server-side. If you're on v3 and only wiring the constraint, you're accepting every score — including 0.1. The constraint alone is not enough; check the score.

enabled: false is the testing story. This is the single most useful knob day-to-day:

# config/packages/test/ewz_recaptcha.yaml
ewz_recaptcha:
    public_key: 'test'
    private_key: 'test'
    enabled: false

With it off, the field still renders (so form definitions and form_widget calls stay valid) but the validator short-circuits and no network call happens. Functional tests that submit forms keep passing in CI with no Google dependency and no flake. Do the same in config/packages/dev/ unless you're specifically exercising the real flow. Note: if you upgrade to v1.6.2 to get Symfony 8 you'll probably also bump your Symfony test kernel, so re-run the suite once even though the bundle's behavior looks identical.

Keep the field unmapped. 'mapped' => false on every usage. The g-recaptcha-response payload is not part of your entity/DTO, and mapping it will silently try to hydrate a property that doesn't exist.

Ajax submission. The bundle's ajax option is designed for forms submitted without a page reload — it wires the token into an XHR submit path and returns a JSON error for the front-end to render. Worth using if you're on Symfony UX/Turbo/Live Components, because re-rendering a form fragment invalidates the token and you must re-run grecaptcha.render() (or grecaptcha.reset()) after the swap. If you're doing partial re-renders, plan the reset explicitly — the bundle won't guess.

Multiple captchas on one page. Each RecaptchaType field gets its own container; the script tag must only be emitted once. If you override the theme block, guard the <script> include with a "already emitted" flag (the bundle's own block does this). Duplicated api.js loads cause the "reCAPTCHA has already been rendered in this element" error.

Theming. Override recaptcha_widget in your own form theme rather than editing vendor files:

{% block recaptcha_widget %}
    <div class="g-recaptcha"
         data-sitekey="{{ public_key }}"
         {% if attr.options is defined %}data-theme="{{ attr.options.theme|default('light') }}"{% endif %}>
    </div>
{% endblock %}

For v3 badges that overlap your sticky footer, hide_badge: true plus your own "protected by reCAPTCHA" text is the standard workaround — but Google's terms require the badge to be visible somewhere, so hide-and-render-elsewhere, not hide-and-forget.

Constraint message & i18n. new RecaptchaTrue(['message' => 'Please confirm you are human.']) — the default message is not translated by Symfony's translator unless you ship the catalogue. Set it explicitly, and set locale in config so the widget itself is localized (defaults to request locale, which is usually what you want).

Wizard/multi-step forms. Verify at the final submit only. Tokens are single-use and expire in ~2 minutes; putting the field on step 1 and the submit on step 3 is a guaranteed failure for anyone who reads slowly.

Gotchas and Tips

  • You are still not the target audience. No Laravel support in 1.6.2, no illuminate/* dependencies, 0 stars and 0 dependents on this fork while upstream ewz-recaptcha-bundle has the docs, issues, and recipe. If you are in Symfony, prefer upstream and pin — unless upstream hasn't shipped its Symfony 8 deprecation pass, in which case this fork's 1.6.2 is currently the differentiator. If you're in Laravel, port it — the siteverify call is the whole product, and this release did not change that call.
  • The one real decision in 1.6.2 is your PHP floor. "Fixes Symfony 8.0 deprecations" almost certainly means the symfony/* constraints now allow ^8.0, and Symfony 8 requires PHP 8.4. Check composer.json before running composer update: on PHP 8.2/8.3 you'll stay on the previous tag, and that's fine — you get zero behavioral benefit from the bump.
  • Upgrading is a no-op for your code, but not free for your test suite. Deprecation fixes touch constructor signatures, @required/autowire attributes, and return types on internals you may have decorated. If you decorated RecaptchaVerifier or the HTTP client, diff the service definitions and re-run the suite once.
  • Block "recaptcha_widget" does not exist. Almost always a form-theme registration problem. Add '@EWZRecaptcha/Form/ewz_recaptcha_widget.html.twig' to twig.form_themes in config/packages/twig.yaml. This bites everyone using Bootstrap 4/5 or a custom theme.
  • Keys are domain-bound. A key generated for example.com returns success: false with hostname mismatch on localhost and on your staging domain. Generate separate key pairs per environment; don't "fix" it in code.
  • verify_host and proxies. This option makes the bundle compare the response hostname against the current request host. Behind a load balancer, a reverse proxy, or a Heroku-style router, the request host may not match what Google saw — either trust your forwarded headers (trusted_proxies / trusted_hosts) or disable the check. Symptom: works locally, fails only in production.
  • CSP will break this silently. The script tag needs script-src https://www.google.com/recaptcha/ https://www.gstatic.com/recaptcha/, the iframe needs frame-src https://www.google.com/recaptcha/, and v3 calls back to https://www.google.com/recaptcha/api2/. Without these the widget renders an empty box and no JS error reaches your logs. Check the browser console before you check PHP.
  • No JS / privacy blockers → empty g-recaptcha-response. The constraint fails with a generic message. Make sure the form shows a field-level error rather than a 500; the null response path must be handled, and a plain required HTML attribute won't do it.
  • Score thresholds are a tuning exercise, not a config value. Start at 0.5, log the score on every submission for a week (the siteverify payload includes it), then move. Setting 0.9 to "be safe" blocks real customers on mobile networks and VPNs. Always log the error-codes array when success is false — invalid-input-secret, timeout-or-duplicate, and invalid-input-response mean three very different things and the bundle surfaces none of them by default.
  • Duplicate/timeout errors are normal. Same token submitted twice (double-click, browser retry, back-button resubmit) → timeout-or-duplicate. Disable the submit button on click, and don't retry the verify call.
  • No timeout on the verify call is a production hazard. Google's endpoint occasionally hangs; without a timeout you pin a PHP-FPM worker. Check the service class for a timeout option and set http_proxy if your egress requires it. In Laravel, Http::timeout(5) and a try/catch that fails open or closed deliberately — pick one and document it.
  • enabled: false is also a security footgun. It's the right call in test/dev, but a stray APP_ENV in production silently disables your bot protection with no warning. Log a startup notice when the bundle is disabled outside dev, or assert on it in a health check.
  • Extension points, in rough order of usefulness: override the Twig recaptcha_widget block → decorate the verifier service to add logging/metrics → swap the constraint's message → replace the whole RecaptchaType service definition if you need extra widget attributes. You can also decorate the HTTP client to add retries or a circuit breaker, which is worth doing if you're seeing Google latency.
  • Fork health, updated. v1.6.2 is the first evidence this fork is doing real framework-tracking work rather than a one-off constraint bump — that's a mild vote of confidence, and it means the "check the diff against upstream" advice now cuts the other way: the diff is a genuine Symfony 8 compat pass, so you may legitimately want this fork over upstream for now. Pin it exactly ("gjuric/recaptcha-bundle": "1.6.2"), watch upstream's changelog, and be ready to hop back the moment upstream ships the equivalent. 0 stars and 0 dependents still means you own the support burden for anything that breaks next Symfony cycle.
Weaver

How can I help you explore Laravel packages today?

Conversation history is not saved when not logged in.
Prompt
Add packages to context
No packages found.
mrnewport/laravel-plaid
mrnewport/laravel-repo
mrnewport/laravel-docsign
mrnewport/laravel-stow
mrnewport/laravel-flow
mrnewport/laravel-priceable
mrnewport/laravel-nestedterms
fredmeng/laravel-apidoc-generator
itstructure/laravel-multilingual-tools
pdustdar/elasticsearch
markupai/php-sdk
rlacerda83/lumem-microservice-shipment
uni-method/symfony-lib
tacticmedia/scim-server-bundle
neika-ai/symfony-bundle
mattleads/agent-publishing-bundle
glavweb/framework-bundle
glavweb/extra-bundle
glavweb/api-bundle
glavweb/admin-bundle