rasuvaeff/yii3-recaptcha

Google reCAPTCHA v2/v3 widget and validator for Yii3.

Maintainers

Package info

github.com/rasuvaeff/yii3-recaptcha

pkg:composer/rasuvaeff/yii3-recaptcha

Statistics

Installs: 0

Dependents: 0

Suggesters: 0

Stars: 0

Open Issues: 0

v1.0.0 2026-06-01 12:55 UTC

This package is auto-updated.

Last update: 2026-06-01 13:09:51 UTC


README

Stable Version Total Downloads Build Static analysis Coverage Psalm level License

Google reCAPTCHA v2 and v3 widget and server-side validator for Yii3.

Provides RecaptchaV2 / RecaptchaV3 widgets for rendering challenges in forms and RecaptchaV2Rule / RecaptchaV3Rule with their handlers for server-side verification through the Yii validator pipeline. HTTP calls go through any PSR-18 client.

Using an AI coding assistant? llms.txt contains a compact API reference you can share with the model. Contributors: see AGENTS.md.

Requirements

Requirement Version
PHP ^8.3
A PSR-18 HTTP client + PSR-17 factories any implementation
yiisoft/widget ^2.2
yiisoft/html ^4.0
yiisoft/validator ^2.5
yiisoft/translator ^3.0
yiisoft/request-provider ^1.3

Installation

composer require rasuvaeff/yii3-recaptcha

You also need a PSR-18 client and PSR-17 factories if your project doesn't already ship one:

composer require guzzlehttp/guzzle nyholm/psr7
# or another PSR-18 client plus PSR-17 factories

Usage

reCAPTCHA v2

use Rasuvaeff\Yii3Recaptcha\RecaptchaV2;
use Rasuvaeff\Yii3Recaptcha\RecaptchaV2Theme;
use Rasuvaeff\Yii3Recaptcha\RecaptchaV2Size;

// siteKey comes from DI config (RecaptchaConfig.siteKeyV2)
echo RecaptchaV2::widget()
    ->withTheme(RecaptchaV2Theme::Dark)
    ->withSize(RecaptchaV2Size::Normal);
use Rasuvaeff\Yii3Recaptcha\RecaptchaV2Rule;

class LoginForm
{
    #[RecaptchaV2Rule]
    public string $gRecaptchaResponse = '';
}

reCAPTCHA v3

use Rasuvaeff\Yii3Recaptcha\RecaptchaV3;

// siteKey comes from DI config (RecaptchaConfig.siteKeyV3)
echo RecaptchaV3::widget();

The v3 widget renders the API <script>, a hidden input for the token, and a script that fills the token on page load (or, with withFormId(), intercepts the form submit, runs grecaptcha.execute() with the configured action, writes the token into the hidden input, then submits — "invisible submit"):

echo RecaptchaV3::widget()
    ->withAction('login')
    ->withFieldName('recaptchaToken')   // hidden input name bound to the model attribute
    ->withFormId('login-form')          // optional: enable invisible-submit binding
    ->withBadge(RecaptchaV3Badge::Hidden); // optional: hide badge + render required legal notice

When the badge is hidden you must keep the reCAPTCHA legal notice visible — the widget renders it for you. All values are JSON-encoded with XSS-safe flags before being embedded in the inline script.

use Rasuvaeff\Yii3Recaptcha\RecaptchaV3Rule;

class LoginForm
{
    #[RecaptchaV3Rule(threshold: 0.5, action: 'login')]
    public string $recaptchaToken = '';
}

Dependency injection (Yii3)

Override params in your application config:

// config/params.php
return [
    'rasuvaeff/yii3-recaptcha' => [
        'siteKeyV2' => $_ENV['RECAPTCHA_SITE_KEY_V2'],
        'secretV2' => $_ENV['RECAPTCHA_SECRET_V2'],
        'siteKeyV3' => $_ENV['RECAPTCHA_SITE_KEY_V3'],
        'secretV3' => $_ENV['RECAPTCHA_SECRET_V3'],
        'sendRemoteIp' => true,
        'translation.category' => 'yii3-recaptcha',
    ],
];

Translations

Locale File
ru messages/ru/yii3-recaptcha.php

To add more languages, create messages/<locale>/yii3-recaptcha.php:

<?php

declare(strict_types=1);

return [
    'The CAPTCHA verification failed.' => 'Your translated message.',
    'The CAPTCHA score is too low.' => 'Your translated message.',
    'The CAPTCHA action does not match.' => 'Your translated message.',
];

Components

RecaptchaV2 (widget)

Method Description
withSiteKey(string $siteKey): self Google site key (required).
withId(string $id): self DOM id for the widget container. Default: auto-generated unique id (supports multiple widgets per page).
withTheme(RecaptchaV2Theme $theme): self Light or Dark. Default: Light.
withType(RecaptchaV2Type $type): self Image or Audio. Default: Image.
withSize(RecaptchaV2Size $size): self Normal, Compact, or Invisible. Default: Normal.
withJsApiUrl(string $url): self Override the script URL.
withCallback(string $cb): self JS callback on success.
withExpiredCallback(string $cb): self JS callback on expiry.
withErrorCallback(string $cb): self JS callback on error.
render(): string Returns HTML. Throws if siteKey is not set.

RecaptchaV3 (widget)

Method Description
withSiteKey(string $siteKey): self Google site key (required).
withAction(string $action): self Action name passed to grecaptcha.execute(). Default: submit.
withFieldName(string $name): self Hidden input name (model attribute). Default: g-recaptcha-response.
withFieldId(string $id): self Hidden input DOM id. Default: auto-generated unique id.
withFormId(string $id): self Enable invisible-submit binding to this form id. Default: none (token filled on load).
withBadge(RecaptchaV3Badge $badge): self Badge position: BottomRight (default), BottomLeft, or Hidden (+ legal notice).
withJsApiUrl(string $url): self Override the script URL.
render(): string Returns HTML (script + hidden input + inline script). Throws if siteKey is not set.

RecaptchaConfig

final readonly class RecaptchaConfig
{
    public function __construct(
        public string $siteKeyV2 = '',
        public string $secretV2 = '',
        public string $siteKeyV3 = '',
        public string $secretV3 = '',
        public string $verifyUrl = 'https://www.google.com/recaptcha/api/siteverify',
        public bool $sendRemoteIp = false,
    ) {}
}

RecaptchaClient

final readonly class RecaptchaClient
{
    public function verify(string $token, ?string $clientIp = null): VerificationResult;     // secretV2
    public function verifyV3(string $token, ?string $clientIp = null): VerificationResult;   // secretV3
    public function verifyWithSecret(string $token, string $secret, ?string $clientIp = null): VerificationResult;
}

verify() uses secretV2, verifyV3() uses secretV3 from config. verifyWithSecret() uses a custom secret (for v2/v3 rules that override it). In the validator pipeline the handlers resolve clientIp from the current request via yiisoft/request-provider (RequestProviderInterface, REMOTE_ADDR) — only when the rule's sendRemoteIp and RecaptchaConfig::sendRemoteIp are both enabled.

VerificationResult

final readonly class VerificationResult
{
    public bool $success;
    public array $errorCodes;   // string[]
    public ?float $score;       // v3 only
    public ?string $action;     // v3 only
    public ?string $hostname;
    public ?string $challengeTs;
}

RecaptchaV2Rule / RecaptchaV2RuleHandler

Parameter Type Default Description
message string 'The CAPTCHA verification failed.' Error message.
secret ?string null Override secret.
sendRemoteIp bool false Forward client IP.
skipOnEmpty bool|callable|null null Skip on empty.
skipOnError bool false Skip on prior error.
when ?Closure null Conditional execution.

RecaptchaV3Rule / RecaptchaV3RuleHandler

Same as v2, plus:

Parameter Type Default Description
threshold float 0.5 Minimum score in the 0.0..1.0 range.
action ?string null Expected action name.
scoreTooLowMessage string 'The CAPTCHA score is too low.' Score error.
actionMismatchMessage string 'The CAPTCHA action does not match.' Action error.

Enums

Enum Values
RecaptchaV2Theme Light, Dark
RecaptchaV2Type Image, Audio
RecaptchaV2Size Normal, Compact, Invisible

Security

  • The widget renders public site keys in HTML — this is intentional.
  • Secrets are server-side only.
  • Token verification goes over HTTPS.
  • v3 score threshold and action validation prevent token reuse across contexts.
  • Widget JS is built with json_encode using JSON_HEX_TAG|JSON_HEX_APOS|JSON_HEX_QUOT|JSON_HEX_AMP, so callback names, actions, ids and other values cannot break out of the inline <script> (no raw string concatenation).
  • sendRemoteIp is opt-in; the client IP comes from the current request via RequestProviderInterface (REMOTE_ADDR), not from user input.

Examples

See examples/ for runnable scripts.

Script Shows Needs server?
widget-v2.php Rendering v2 widget no
widget-v3.php Rendering v3 widget no

Development

No PHP/Composer on the host — run in Docker via the composer:2 image:

docker run --rm -v "$PWD":/app -w /app composer:2 composer install
docker run --rm -v "$PWD":/app -w /app composer:2 composer build
docker run --rm -v "$PWD":/app -w /app composer:2 composer cs:fix
docker run --rm -v "$PWD":/app -w /app composer:2 composer test

make test-coverage and make mutation bootstrap pcov inside the Docker container because the base composer:2 image does not ship with a coverage driver.

Or with Make:

make install
make build
make cs-fix
make test

CI runs composer build on PHP 8.3, 8.4, and 8.5.

License

BSD-3-Clause