lethak/excelwebzonerecaptcha-bundle
Integrates Google reCAPTCHA into PHP/Symfony forms via the ExcelWebZone bundle, offering configurable widget rendering and server-side verification to help protect applications from spam, bots, and automated form abuse.
First, the framework reality check. lethak/excelwebzonerecaptcha-bundle is a Symfony bundle, not a Laravel package. There is no service provider, no config/recaptcha.php, no extra.laravel auto-discovery, and no Blade/Twig interoperability in this codebase. composer require will pull in Symfony components (symfony/form, symfony/http-kernel, symfony/validator, symfony/dependency-injection) that will sit unused in a Laravel app, and its EWZRecaptchaType references Symfony\Component\Form\AbstractType — nothing in Laravel's Illuminate stack knows what to do with that.
Three realistic paths for a Laravel developer:
google/recaptcha (the official client) directly, or a Laravel-native package such as anhskohbo/no-captcha. You get the same siteverify call with a fraction of the dependency surface.Validator/Constraints/RecaptchaValidator.php (server-side verify) and the widget rendering template (client-side grecaptcha.render/execute). That's ~100–150 lines to reimplement idiomatically.If you genuinely need a working captcha today, the minimal Laravel path is:
composer require google/recaptcha
// config/services.php
'recaptcha' => [
'site_key' => env('RECAPTCHA_SITE_KEY'),
'secret_key' => env('RECAPTCHA_SECRET_KEY'),
],
Then a Rule object wrapping ReCaptcha::verifyResponse() — see the next section. First use case: protect registration or contact forms where a spam bot would create rows in your DB or trigger transactional email.
Interface-first, so the vendor is swappable. Define a contract and bind a Google implementation. This is the single most important pattern — it's what makes the choice reversible.
// app/Contracts/CaptchaVerifier.php
interface CaptchaVerifier {
public function verify(string $token, ?string $action = null, ?string $ip = null): bool;
}
// app/Services/GoogleCaptchaVerifier.php
class GoogleCaptchaVerifier implements CaptchaVerifier
{
public function __construct(
private string $secret,
private float $minScore = 0.5,
) {}
public function verify(string $token, ?string $action = null, ?string $ip = null): bool
{
$res = (new ReCaptcha($this->secret))->setExpectedHostname(request()->getHost())->verify($token, $ip ?? request()->ip());
if (!$res->isSuccess()) return false;
if ($action !== null && $res->getAction() !== $action) return false;
return $res->getScore() === null || $res->getScore() >= $this->minScore;
}
}
Wrap in a Validation Rule, not middleware. Laravel's validation pipeline is where per-form captcha belongs, because captcha is form-scoped (its token is single-use) and middleware cannot easily distinguish "user submitted form" from "user submitted form with a token to check."
class ValidRecaptcha implements ValidationRule {
public function __construct(private string $action = 'submit') {}
public function validate($attribute, $value, $fail): void {
if (!app(CaptchaVerifier::class)->verify((string) $value, $this->action, request()->ip())) {
$fail('Captcha verification failed.');
}
}
}
Blade rendering. The bundle's Twig helper translates to a Blade component. Load Google's script once, render the widget by site_key, and put the token into a hidden input that matches the rule above.
{{-- resources/views/components/captcha.blade.php --}}
@once @push('scripts')
<script src="https://www.google.com/recaptcha/api.js" async defer></script>
@endpush @endonce
<div class="g-recaptcha" data-sitekey="{{ config('services.recaptcha.site_key') }}"></div>
For v3, use grecaptcha.execute on submit and inject the resulting token into a hidden field; verify the action string server-side to prevent token reuse across forms.
Feature-flag the rollout. Captcha failures block submissions — that's the highest-severity failure mode on a conversion-critical path. Ship behind config('features.captcha') with a bypass for internal/staging traffic, canary on one low-traffic form, then widen.
Fail-open vs. fail-closed per form. Contact form → fail-open (log, let it through). Registration/checkout guest → fail-closed. Put the policy on the verifier, not in each rule.
Testing. Stub CaptchaVerifier via $this->app->instance(...) in feature tests. Never hit Google from tests — they'll rate-limit you and slow the suite. Add one integration test that runs against real keys on a nightly build to catch broken siteverify contracts.
EWZRecaptchaBundle registered via a Symfony kernel shim, its form type expects Symfony\Component\Form\FormBuilderInterface. Laravel request objects won't satisfy the type hints. You'll spend more time on the adapter than on writing the 100 lines from scratch.site_key belongs in HTML. Audit for {{ config('services.recaptcha.secret_key') }} in views and for Log::info($response) in the verifier catching raw response bodies.siteverify is a synchronous HTTPS call (50–200 ms typical). Set explicit timeouts on the Guzzle client (connect_timeout: 2, timeout: 3) and decide what happens on timeout — google/recaptcha returns false from isSuccess() on network failure by default, which means your form fails-closed. Log and alert on the error rate so you notice a Google outage rather than only noticing "registration is down."g-recaptcha widget). v2 returns a boolean; v3 returns a score and an action. If you're porting, don't copy the boolean-only logic and expose it as v3-capable — getScore() and getAction() are mandatory checks for v3 or tokens can be replayed across forms. Anchor v3 thresholds in config (services.recaptcha.min_score) and monitor rejection rate as a product metric, because threshold drift silently blocks legitimate users.verify_url is hard-coded to https://www.google.com/recaptcha/api/siteverify. If you're building from the bundle's reference, keep that URL configurable (a RECAPTCHA_VERIFY_URL env var) so you can swap to a regional endpoint or a mock server in tests without code changes.throttle:5,1 on the endpoint; it doesn't replace it. Bots that pass the checkbox (human farms, compromised browsers) will hammer your form at line rate. Run both.siteverify and asserts the response shape — if the secret rotates wrong, you'll find out in an alert instead of in the support inbox.message option (match that for your $fail() string so translations work), and the validator accepts an injectable HttpClient interface (match that with an Http facade fake in tests so you can assert the outbound call shape).How can I help you explore Laravel packages today?