tigusigalpa/yandex-smartcaptcha-php
PHP client for Yandex SmartCaptcha that handles server-side token validation and helps render the client widget in your forms. A simple, framework-friendly way to block bots and spam without adding friction for real users.
Install and inspect
composer require tigusigalpa/yandex-smartcaptcha-php
Before writing any code, read, in this order:
README.md (English) — the quick-start block is the canonical wiring.examples/ — frontend widget markup + a custom validation rule example; this is the fastest path to a working form.config/ and the Service Provider — confirms which env var names the package actually reads (do not guess these).src/SmartCaptchaClient.php plus the CaptchaInfo, ValidationResult, SecretKey DTOs — the type hints tell you exactly what you get back.Configure
Publish the config using the tag printed by php artisan vendor:publish (the release notes don't name it — list tags rather than guessing). Then set two distinct credential groups in .env:
# Server-side token validation (the common case)
YANDEX_SMARTCAPTCHA_SERVER_KEY=ysc1_...
# Management API only (create/read/update/delete captchas, fetch secrets)
YANDEX_SMARTCAPTCHA_OAUTH_TOKEN=...
The client-site key is public and belongs in your Blade template, not .env secret storage — but keep it in config so staging/production widgets differ.
First use case: protect one form
Render the widget, post the token, validate server-side. This is the 20-minute win and covers 90% of real usage.
{{-- resources/views/auth/login.blade.php --}}
<form method="POST" action="/login">
@csrf
<div id="captcha-container"></div>
<script src="https://smartcaptcha.yandexcloud.net/captcha.js" defer></script>
<script>
window.addEventListener('load', () => {
window.smartCaptcha.render('captcha-container', {
sitekey: @json(config('yandex-smartcaptcha.client_key')),
hl: 'en',
});
});
</script>
</form>
$result = app(SmartCaptchaClient::class)->validate(
token: $request->input('smart-token'),
secret: config('yandex-smartcaptcha.server_key'),
ip: $request->ip(),
);
if (! $result->isValid()) {
// $result likely exposes an error/reason — log it, show generic message
}
Note the widget's form field name is smart-token (hyphenated) — it will not appear in $request->validated() unless you account for it.
Wrap validation in a Rule, not a controller
The single highest-leverage pattern: one SmartCaptcha rule object that resolves the client from the container, calls validate(), and returns a human-readable failure. Then every FormRequest in the app gets captcha protection with one line, and you can unit-test the rule with a bound fake.
final class SmartCaptcha implements ValidationRule
{
public function validate(string $attribute, mixed $value, Closure $fail): void
{
if (! is_string($value) || $value === '') {
$fail('captcha.required')->translate();
return;
}
$result = app(SmartCaptchaClient::class)->validate(
token: $value,
secret: config('yandex-smartcaptcha.server_key'),
ip: request()->ip(),
);
if (! $result->isValid()) {
$fail('captcha.invalid')->translate();
}
}
}
Inject, never new
The release notes show new SmartCaptchaClient($oauthToken) for a bare PHP script. Inside Laravel, bind through the container (the Service Provider should already do this) so the OAuth/IAM token manager, logger, and HTTP client are shared and refresh state isn't duplicated per request.
public function __construct(private SmartCaptchaClient $captcha) {}
Treat the management API as ops, not request-path
CRUD on captchas and secret-key retrieval require IAM/OAuth — a heavier, slower, rate-limited credential. Put that work in an artisan command or deploy hook: create a captcha per environment, rotate secrets, list existing ones to detect drift. Never call it inside a user request. Cache retrieved secrets with a TTL shorter than the 12-hour IAM window so rotation is picked up.
// app/Console/Commands/SyncCaptcha.php
$captchas = $client->list(); // audit what exists
$secret = $client->getSecretKey($id); // store in your secret manager
Layer defensive middleware for high-risk routes
For login, registration, password reset, and public comment endpoints, apply a middleware in addition to the rule so a missing/broken widget fails closed rather than silently skipping verification.
Test by substituting the client
Bind a stub that returns a ValidationResult fixture. Assert both branches of each FormRequest. Keep a couple of tests hitting a mocked HTTP layer to cover DTO hydration and error mapping — that's where this package will actually surprise you.
Log outcomes, not tokens
Pass a PSR-3 logger into the client via config. Log isValid() plus the failure reason and the route; never log the raw token (it's a bearer-ish artifact) or the server key.
validate() needs the server secret key. The management API needs an OAuth/IAM token from yandex-cloud-client-php. Configuring only one and expecting both to work is the #1 first-day error.localhost unless you've added it. If nothing renders, check this before debugging PHP.$request->ip() is only meaningful behind correctly configured TrustProxies. A spoofable X-Forwarded-For weakens the signal; a wrong IP (proxy, load balancer) can cause false negatives.GuzzleException become a 500 on your login page."tigusigalpa/yandex-smartcaptcha-php": "^1.0" and read the changelog before minor bumps. DTO constructors and error shapes are the most likely thing to shift in a 1.x.token:, secret:, ip:. Keep them — positional order across a validate() signature is exactly what breaks quietly on upgrade.hl option and the package ships EN/RU docs; match the widget language to your app locale rather than leaving it defaulted.ValidationResult into your own domain object at the boundary so a future package change doesn't leak into controllers).How can I help you explore Laravel packages today?