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.
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:
src/ReCaptcha.php — setOptions(), getHTML(), verify(), getChallengeFields(). Everything you'll actually use lives here.src/Response.php — the value object returned by verify(): isValid(), getErrorCodes(), getErrorMessage().src/MailHide.php — only if you need email obfuscation; most projects don't.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.
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.
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.
How can I help you explore Laravel packages today?