pwiejak/recaptcha-bundle
pwiejak/recaptcha-bundle integrates Google reCAPTCHA into Symfony applications, helping protect forms and endpoints from spam and automated abuse. It offers straightforward configuration, server-side verification, and simple service integration for adding reCAPTCHA checks to your project.
Reality check before you install anything: this is a Symfony bundle (a fork of ewz/recaptcha-bundle), not a Laravel package. There is no service provider, no facade, no config/recaptcha.php, and it will not autoload into a Laravel app. As a Laravel developer your actual day-to-day leverage is one of three things:
If you only write Laravel, skip to "Implementation Patterns" for the porting notes and use biscolab/laravel-recaptcha instead.
Minimal Symfony setup (what the package actually expects):
composer require pwiejak/recaptcha-bundle
Then, in order:
Find the real namespace first. Forks frequently rename the vendor namespace but leave the bundle class name. Grep before you guess:
grep -r "class .*Bundle" vendor/pwiejak/recaptcha-bundle/src/
Register whatever that returns in config/bundles.php:
return [
// ...
Pwiejak\RecaptchaBundle\PwiejakRecaptchaBundle::class => ['all' => true],
];
Find the config root key. Upstream EWZ uses ewz_recaptcha:; a fork may keep that key verbatim (common, to avoid breaking copied configs) or rename it. Check src/DependencyInjection/Configuration.php — the ->rootNode() / tree builder name is authoritative.
Configure with env vars, never literals:
# config/packages/recaptcha.yaml
ewz_recaptcha: # verify this key against Configuration.php
public_key: '%env(RECAPTCHA_PUBLIC_KEY)%'
private_key: '%env(RECAPTCHA_PRIVATE_KEY)%'
enabled: true
locale: '%kernel.default_locale%'
First use case — a contact form checkbox:
$builder->add('recaptcha', RecaptchaType::class);
The bundle supplies a form type (renders the widget + JS) and a constraint (IsTrue upstream) that calls Google's siteverify endpoint server-side during form validation.
If you're in Laravel and need this working today, wire it manually — it's about 15 lines:
// app/Rules/Recaptcha.php
public function passes($attribute, $value): bool
{
return Http::asForm()->post('https://www.google.com/recaptcha/api/siteverify', [
'secret' => config('services.recaptcha.secret'),
'response' => $value,
'remoteip' => request()->ip(),
])->json('success', false);
}
That is genuinely the whole feature. The bundle's value is the form-type/twig plumbing around it, which Laravel doesn't share.
Symfony-side patterns (what the bundle is built for):
Attach to every public form, not just login. The idiomatic placement is a reusable form type or a form extension so you don't hand-add the field to 12 forms. Upstream EWZ supports an enabled flag, so gate it per-environment:
# config/packages/test/recaptcha.yaml
ewz_recaptcha:
enabled: false
This is the single most useful pattern — tests and local dev skip the network call entirely.
Invisible reCAPTCHA is configured by a flag on the field/type rather than a different type. You bind it to the submit button. Verify the exact option name in the fork's RecaptchaType::configureOptions(); upstream uses as_invisible / attr keys, but forks do drift here.
Validation lives in the constraint, not the controller. You don't call siteverify yourself; you $form->isValid() and the constraint handles it. This is the Symfony analogue of a Laravel FormRequest rule — same mental model: validation is declarative and colocated with the form.
Bridging into a mixed stack. If you have Laravel fronting a Symfony admin app, keep reCAPTCHA isolated to the Symfony side and let Laravel verify its own forms independently. Don't try to share one widget across a Blade view and a Twig view; you'll get duplicate grecaptcha script includes and unpredictable rendering.
Laravel ↔ bundle concept map when porting:
| Symfony bundle | Laravel equivalent |
|---|---|
Bundle registration (bundles.php) |
Auto-discovery via composer.json extra.laravel |
config/packages/*.yaml |
config/services.php |
RecaptchaType form field |
Blade component / {!! recaptcha_field() !!} |
IsTrue validator constraint |
Custom validation Rule |
enabled: false in test config |
Recaptcha::shouldReceive(...) or Http::fake() |
Testing pattern. In Symfony functional tests, disable via config (above) and assert the field exists rather than asserting Google's response. Never let CI hit google.com/recaptcha/api/siteverify — it will rate-limit and flake.
Laravel-side patterns, if you're porting the idea:
Rule and reuse it across FormRequests; fail closed (return false on network error) rather than open.Http::fake() in tests so the rule is deterministic.Zero stars, zero dependents is a real risk signal. This is a personal fork. Check whether it tracks upstream ewz/recaptcha-bundle commits; if it's a one-off snapshot, you're pinned to whatever Symfony version it was forked against. Prefer upstream EWZ unless the fork fixes something you specifically need, and vendor/pin the exact version.
Namespace and config-key drift are the #1 time sink. Forks of EWZ often keep the ewz_recaptcha config root while renaming the PHP namespace. Every doc snippet you find online for EWZ will use the old namespace. Always read Configuration.php and the src/ directory rather than trusting blog posts.
Secrets leak easily here. These bundles read keys from YAML, and people inline them. Always '%env(...)%', and add both keys to .env.example and your secret manager. A reCAPTCHA secret in a committed YAML is a live credential.
The public key must be registered for the exact hostname. localhost behaves specially — Google ships official test keys (6LeIxAcTAAAAAJcZVRqyHh71UMIEGNQ_MXjiZKhI / 6LeIxAcTAAAAAGG-vFI1TnRWxMZNFuojJ4WifJWe) that always pass. Use them in local/staging instead of disabling validation, so you exercise the real code path.
Client-side success proves nothing. The widget can be spoofed; only the server-side siteverify call matters. If form submission works with JS disabled, the constraint isn't wired up.
Token is single-use and expires (~2 min). Don't cache the response and don't re-validate the same token in two places (e.g., a Laravel controller and a Symfony listener). This bites people who add reCAPTCHA at both the form layer and an API gateway.
Asset/JS inclusion is the usual rendering gotcha. If the widget renders as an empty div, the grecaptcha script isn't being emitted — check the Twig form theme is registered and that your layout actually renders form_rest/the block. In webpack setups, confirm the bundle's JS isn't being tree-shaken away.
Extension points, if you do adopt it: the form type can be extended with a custom form theme block, and the constraint is a normal Symfony validator, so you can decorate/subclass it. If you need v3 or Enterprise, that's a fork of the constraint, not a config flag.
After any config change: bin/console cache:clear. Symfony config caching will happily serve your old broken key for an hour otherwise.
Final recommendation for a Laravel-only shop: don't adopt this. The integration surface you'd be paying for (form type, Twig widget, DI config) is entirely absent in Laravel, and the fork is unmaintained-looking. Use biscolab/laravel-recaptcha, or the ~15-line Http::asForm() rule above, and you'll have fewer moving parts and a package that actually autoloads.
How can I help you explore Laravel packages today?