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

Zendservice Recaptcha Laravel Package

zendframework/zendservice-recaptcha

A legacy Zend Framework component for integrating Google reCAPTCHA into PHP applications, with service classes to render CAPTCHA challenges, send validation requests, and handle API responses server-side.

View on GitHub
Deep Wiki
Context7

Getting Started

This is the namespaced extraction of ZF1's Zend_Service_ReCaptcha — a thin OOP wrapper around the reCAPTCHA v1 HTTP API. It gives you three things: a widget renderer (getHTML()), a server-side verifier (verify()), and a MailHide helper for obfuscating email addresses. That's the whole surface area.

composer require zendframework/zendservice-recaptcha
# Composer will warn: Package zendframework/zendservice-recaptcha is abandoned

Where to look first, in this order:

  1. src/ReCaptcha.php — setOptions(), getHTML(), verify(), getChallengeFields(). Everything you'll actually use lives here.
  2. src/Response.php — the value object returned by verify(): isValid(), getErrorCodes(), getErrorMessage().
  3. src/MailHide.php — only if you need email obfuscation; most projects don't.
  4. composer.json — check the PHP constraint before promising this to anyone. The 2019 tag predates PHP 8.

First use case — verify a legacy form post that carries the two v1 fields:

use ZendService\ReCaptcha\ReCaptcha;

$recaptcha = new ReCaptcha([
    'public_key'  => getenv('RECAPTCHA_PUBLIC_KEY'),
    'private_key' => getenv('RECAPTCHA_PRIVATE_KEY'),
]);

// Render the widget in your template:
echo $recaptcha->getHTML();

// Verify on POST:
try {
    $result = $recaptcha->verify(
        $_POST['recaptcha_challenge_field'] ?? '',
        $_POST['recaptcha_response_field'] ?? ''
    );
} catch (\ZendService\ReCaptcha\Exception\RuntimeException $e) {
    // transport failure — fail closed
    $result = null;
}

if (!$result || !$result->isValid()) {
    $errors = $result ? $result->getErrorCodes() : ['transport-error'];
}

Use getChallengeFields() rather than hard-coding the POST keys if you're wiring this into a form library — it returns the field names the widget actually emits.

Implementation Patterns

Configuration-first construction. Keys should never be literals. In Laravel, put them in config/services.php (sourced from .env) and bind a configured instance in a service provider so the rule/controller doesn't care:

$this->app->singleton(\ZendService\ReCaptcha\ReCaptcha::class, fn () => new \ZendService\ReCaptcha\ReCaptcha([
    'public_key'  => config('services.recaptcha.public_key'),
    'private_key' => config('services.recaptcha.private_key'),
]));

Wrap verification in a validation rule, not in the controller. This keeps the failure path identical to every other validation error and makes it testable:

namespace App\Rules;

use Illuminate\Contracts\Validation\Rule;
use ZendService\ReCaptcha\ReCaptcha;

class ReCaptchaV1 implements Rule
{
    public function __construct(private ReCaptcha $recaptcha) {}

    public function passes($attribute, $value): bool
    {
        try {
            return $this->recaptcha
                ->verify(request('recaptcha_challenge_field', ''), (string) $value)
                ->isValid();
        } catch (\Throwable $e) {
            report($e);
            return false; // fail closed
        }
    }

    public function message(): string
    {
        return 'Captcha verification failed. Please try again.';
    }
}

Inject the HTTP client for tests. The wrapper accepts a client instance, which is the single most useful extension point in the package. Point it at the bundled test adapter during CI so verify() never touches the network:

$client = new \Zend\Http\Client(null, ['adapter' => \Zend\Http\Client\Adapter\Test::class]);
$client->getAdapter()->setResponse(
    "HTTP/1.1 200 OK\r\n\r\n" . "true\nsuccess"
);
$recaptcha->setHttpClient($client);

Blade/HTML integration. getHTML() returns the full <script> + <noscript> block. Render it inside the form, adjacent to the submit button, and keep the raw echo ({!! !!}) — escaping it produces garbage.

MailHide, if you're using it for a public contact address:

$mailHide = new \ZendService\ReCaptcha\MailHide($publicKey, $privateKey);
echo $mailHide->getHtml('[email protected]');

Note this depends on the same retired v1 challenge service, so it's decorative in practice.

Error-code mapping. getErrorCodes() returns Google's raw strings (incorrect-captcha-sol, invalid-site-private-key, invalid-request-cookie). Map them to distinct user messages — "wrong answer" vs. "misconfigured keys" are very different problems, and conflating them will cost you an hour of debugging every time.

Gotchas and Tips

reCAPTCHA v1 was shut down in March 2018. This is the headline gotcha and it dominates everything else: verify() posts to the retired v1 endpoint, so against the live service it will never return a valid response. No amount of configuration fixes this. If you are evaluating this package for new work, stop and use google/recaptcha (v2 checkbox / invisible / v3 score-based) instead. Keep this package only in a codebase that already depends on it and is being maintained, not rebuilt.

Archived, abandoned, and 0 dependents. Last release 2019-02-07, repo archived. Composer flags it abandoned on require and install. Its transitive dependencies (zend-http, zend-json, zend-uri, zend-loader, …) are all abandoned too, so you inherit a second layer of dead code. Look for the Laminas continuation of this component before forking anything yourself.

PHP version wall. The 2019 constraint predates PHP 8.x. Expect composer require to fail on modern runtimes; --ignore-platform-reqs or a config.platform override may let it install, but untyped/removed-function breakage in zend-http will surface at runtime, not install time. Verify under your actual PHP version before writing any code against it.

Fail closed on transport exceptions. verify() can throw ZendService\ReCaptcha\Exception\RuntimeException (DNS, TLS, timeout). An uncaught exception in a form handler is a 500 for legitimate users; a silent true fallback is an open door. Catch, log, reject.

Validate presence before calling out. Empty challenge/response fields guarantee an invalid result and waste a network round-trip. Check $request->filled(...) first and short-circuit.

Missing keys throw. Both public and private keys must be set before getHTML() or verify(); you'll get an InvalidArgumentException, usually at render time, usually in production because someone forgot the env var. Assert configuration at boot.

Mixed content. getHTML() emits markup pointing at http:// endpoints from the v1 era. On an HTTPS site this is an immediate mixed-content block — you'll need to subclass and override the template, or post-process the returned string.

TLS verification with old zend-http. Defaults and CA bundle paths from that era are unreliable on modern systems; if you're keeping this alive, explicitly configure the adapter (sslverifypeer, sslcapath, timeout) rather than trusting defaults. Also set a short timeout — captcha verification should never be the slowest part of a request.

Testing. Never hit Google in CI, even for the deprecated path. Use the Test adapter with canned true\nsuccess / false\nincorrect-captcha-sol bodies and assert both the valid and invalid branches, plus the exception branch.

Pin it, vendor it if needed. If you're forced to keep the dependency, pin the exact version ("zendframework/zendservice-recaptcha": "2.0.0" style, no caret), and consider committing vendor/ or a fork to a private VCS if packagist availability matters for your build.

Extension points are thin. Subclassing to override getHTML() and swapping the injected Zend\Http\Client are the only two levers worth knowing. There's no adapter interface, no PSR-18/PSR-7 support, no DI container awareness — it's a direct port of a 2009 design.

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.
tequilarapido/trackit
gregsphotography/site-builder-filament
cabaoctavio/laravel-query-log
gregsphotography/site-builder-components
io-digital/clickatell
snowsoft/laravel-admin
oxygen/auth
relicsoftware/barakah-payment-gateway
tobya/work-with-composer
equipc/laravel-email-validator
equipc/laravel-email-database-log
adedsec/nova-translated-labels
wazza/laravel-db-encryption
hokny/laravel-generator
vonsogt/backup
jengo/queues
jeremykenedy/laravel-observability
coleus/frame
thtg88/journalism
filagora/filagora-ai-forums