codein / recaptcha-enterprise-bundle
Symfony integration for Google reCAPTCHA Enterprise (Assessments API). Fork of artack/recaptcha-enterprise-bundle.
Package info
github.com/Codein-Labs/recaptcha-enterprise-bundle
Type:symfony-bundle
pkg:composer/codein/recaptcha-enterprise-bundle
Requires
- php: ^8.1
- symfony/form: ^5.4 || ^6.4 || ^7.4 || ^8.0
- symfony/framework-bundle: ^5.4 || ^6.4 || ^7.4 || ^8.0
- symfony/http-client: ^5.4 || ^6.4 || ^7.4 || ^8.0
- symfony/http-foundation: ^5.4 || ^6.4 || ^7.4 || ^8.0
- symfony/validator: ^5.4 || ^6.4 || ^7.4 || ^8.0
- twig/twig: ^3.0
Requires (Dev)
- phpunit/phpunit: ^10.5 || ^11.0
- roave/security-advisories: dev-latest
- symfony/twig-bridge: ^5.4 || ^6.4 || ^7.4 || ^8.0
Suggests
None
Provides
None
Conflicts
None
Replaces
None
This package is not auto-updated.
Last update: 2026-09-05 01:47:07 UTC
README
Symfony integration for Google reCAPTCHA Enterprise (Assessments API).
Based on artack/recaptcha-enterprise-bundle 0.2.0, forked and maintained by Codéin. Originally developed by ARTACK WebLab GmbH in Zurich, Switzerland, and released under the MIT licence, which this fork keeps along with the original copyright notice.
What changed from artack/recaptcha-enterprise-bundle
- PHP version reduced to 8.1, down from 8.2.
- Symfony support starting with version 5.4, alongside the 6.4 and 7.4 LTS lines and 8.x.
- Added support for the checkbox mode, the "I'm not a robot" widget, beside the invisible score challenge.
- The challenge is chosen once for the whole application through the
challengesetting: a score key and a checkbox key are different key types, andsite_keyis a single global value. - Google's JavaScript resources are no longer loaded automatically and must be added to the layout by hand, so the application decides when — which is what makes GDPR consent manageable.
- Three interfaces are exposed as autowired services:
Assessment\GatewayInterface— the call to Google, depending solely on the Symfony HTTP client, no SDK.Verifier\VerifierInterface— verifying the response, outside the constraint if you need it.CaptchaFailure\FinderInterface— adding your own logic when a captcha is refused.
See "Upgrading from artack/recaptcha-enterprise-bundle:0.2.0" for the full list of breaking changes.
Installation
Requirements
| Requirement | Supported versions |
|---|---|
| PHP | 8.1, 8.2, 8.3, 8.4 |
| Symfony | 5.4 LTS, 6.4 LTS, 7.4 LTS, 8.x |
Installing with the Flex recipe
$ composer config extra.symfony.allow-contrib true
$ composer require codein/recaptcha-enterprise-bundle
The first command is needed once per application: contributed recipes are disabled in the Symfony skeleton.
The recipe then registers the bundle, writes
config/packages/codein_recaptcha_enterprise.yaml, adds the environment variables to .env, and inserts
the bundle's own asset tag into templates/base.html.twig. It never adds Google's tag, which depends on consent.
⚠️ The application loads Google's
enterprise.jsitself. With no loader there is no token, so the constraint refuses every submission and the visitor is locked out of the form. Read "Adding the scripts to your layout" before deploying.
Installing without a recipe
Flex enables the bundle by itself, but it writes no configuration when there is no recipe. Create
config/packages/codein_recaptcha_enterprise.yaml as shown under "Configuration", and add the three
environment variables it reads to your .env:
CODEIN_RECAPTCHA_ENTERPRISE_PROJECT_ID= CODEIN_RECAPTCHA_ENTERPRISE_SITE_KEY= CODEIN_RECAPTCHA_ENTERPRISE_API_KEY=
If the configuration key is rejected as unrecognised, the bundle is not registered — check config/bundles.php
and add it by hand, which is what an install from a VCS repository of a package renamed locally may need:
// config/bundles.php return [ // ... Codein\RecaptchaEnterpriseBundle\CodeinRecaptchaEnterpriseBundle::class => ['all' => true], ];
Adding the scripts to your layout
The bundle ships the submission handling as an asset and never places Google's script on a page —
the application does. Both tags are part of installing the bundle, not an optional extra: with no loader there is
no token, so the constraint refuses every submission as MISSING and the visitor is locked out of the form.
Publish the asset, which Flex's auto-scripts already does on every composer install:
$ php bin/console assets:install public
Expose the site key to Twig:
# config/packages/twig.yaml twig: globals: recaptcha_site_key: '%codein_recaptcha_enterprise.site_key%'
Then add both scripts once per page, in your layout, the Google one only after the visitor has consented:
{# score: the site key is bound to the loader at load time #} <script src="https://www.google.com/recaptcha/enterprise.js?render={{ site_key }}&hl=fr&onload=codeinRecaptchaOnload" async defer></script> {# checkbox: the widgets are rendered explicitly, one per field #} <script src="https://www.google.com/recaptcha/enterprise.js?render=explicit&hl=fr&onload=codeinRecaptchaOnload" async defer></script> {# both challenges: the bundle's submission handling, no consent needed #} <script src="{{ asset('bundles/codeinrecaptchaenterprise/recaptcha-enterprise.js') }}" defer></script>
The bundle's own asset carries no personal data and may be loaded unconditionally; only the Google tag is subject
to consent. The checkbox challenge uses render=explicit instead of the site key. See "Loading the scripts" for
that variant, for the hl= language parameter, for the codeinRecaptchaOnload readiness contract, and for what
an application must do while consent is absent.
Configuration
Create config/packages/codein_recaptcha_enterprise.yaml with your Google project credentials:
# config/packages/codein_recaptcha_enterprise.yaml codein_recaptcha_enterprise: enabled: true # set to false to skip every assessment site_key: '%env(CODEIN_RECAPTCHA_ENTERPRISE_SITE_KEY)%' project_id: '%env(CODEIN_RECAPTCHA_ENTERPRISE_PROJECT_ID)%' api_key: '%env(CODEIN_RECAPTCHA_ENTERPRISE_API_KEY)%' min_score: 0.5 # default score threshold used by the validator when none is provided challenge: score # score (default) or checkbox, see "Choosing the challenge" on_error: deny # deny (default) or allow, see "When Google cannot be reached" http_client_service: codein_recaptcha_enterprise.client # see "Configuring the HTTP client" when@dev: codein_recaptcha_enterprise: enabled: false # disable reCAPTCHA in dev environments
site_key, project_id and api_key are required. min_score defaults to 0.5 and is used when a constraint
does not define its own threshold; set it to 0 to disable the score check entirely, which is what the checkbox
challenge normally wants — see "Choosing the challenge".
⚠️ Google has eleven score levels between
0.0and1.0, but a project without a billing account only ever receives four of them:0.1,0.3,0.7and0.9. A threshold of0.5there means0.7in practice, which is much stricter than it reads.
Configuring the HTTP client
The bundle declares its own scoped client, codein_recaptcha_enterprise.client, and calls Google through it:
framework: http_client: scoped_clients: codein_recaptcha_enterprise.client: base_uri: 'https://recaptchaenterprise.googleapis.com' timeout: 2.0 max_duration: 5.0
The timeouts matter: left to default_socket_timeout, an unresponsive Google holds the worker
instead of reaching the on_error policy quickly.
Redeclare that key in your own framework.yaml to change anything about the transport — an application's
configuration wins over what a bundle prepends. Every scoped_clients option applies, not just the two above:
framework: http_client: scoped_clients: codein_recaptcha_enterprise.client: base_uri: 'https://recaptchaenterprise.googleapis.com' timeout: 5.0 proxy: '%env(HTTPS_PROXY)%' verify_peer: false # e.g. behind a TLS-inspecting corporate proxy retry_failed: max_retries: 2
This is why the bundle exposes no timeout, proxy or verify_peer setting of its own: forwarding transport
options one by one would always lag behind what the HTTP client already supports.
Use http_client_service to point the gateway at an entirely different client instead — an existing scoped
client, a decorated one, or plain http_client:
codein_recaptcha_enterprise: http_client_service: my_app.google_client
Choosing the challenge
challenge sets what the form type renders, for the whole application. It is not a form option:
site_key is a single global value and the two challenges need different key types, so a field asking
for the other one would send the wrong key and Google would refuse it with KEY_MISMATCH.
| Value | What the visitor sees | What Google returns |
|---|---|---|
score (default) |
Nothing. The token is fetched on submit. | A risk analysis score, judged against min_score. |
checkbox |
The "I'm not a robot" checkbox. | A valid or invalid token, plus a risk analysis score. |
⚠️ The two are not interchangeable: a score key and a checkbox key are different key types in the Google console. Pointing
site_keyat the wrong one makes every assessment fail withKEY_MISMATCH.
Checkbox keys are scored too — Google returns a score "regardless of the key type" — but on this challenge
the verdict is the validity of the token: the visitor solved the challenge Google itself decided to set. A threshold on
top of that refuses someone who passed it, and the widget offers no second attempt, so set min_score: 0:
codein_recaptcha_enterprise: challenge: checkbox site_key: '%env(CODEIN_RECAPTCHA_ENTERPRISE_SITE_KEY)%' # must be a checkbox key min_score: 0
The checkbox is rendered explicitly rather than through the usual <div class="g-recaptcha">. Auto-rendering
posts the token in a g-recaptcha-response field at the root of the form data, outside the Symfony field name
prefix, where the constraint would never see it. Explicit rendering writes the token into the bundle's own hidden
field instead, so the validator, the verifier and the constraint stay unaware of which challenge was used.
Checkbox tokens expire after about two minutes, and the field is cleared when that happens, so a stale token is never submitted.
Usage
Render the token field in a Symfony form:
use Codein\RecaptchaEnterpriseBundle\Form\RecaptchaEnterpriseType; use Codein\RecaptchaEnterpriseBundle\Validator\RecaptchaEnterprise; use Symfony\Component\Form\AbstractType; use Symfony\Component\Form\Extension\Core\Type\EmailType; use Symfony\Component\Form\Extension\Core\Type\TextareaType; use Symfony\Component\Form\FormBuilderInterface; final class ContactType extends AbstractType { public function buildForm(FormBuilderInterface $builder, array $options): void { $builder ->add('email', EmailType::class) ->add('message', TextareaType::class) ->add('recaptchaToken', RecaptchaEnterpriseType::class, [ 'action_name' => 'contact', // sent to Google; also matched when validating 'constraints' => [ new RecaptchaEnterprise( minScore: 0.7, // optional actionName: 'contact', ), ], ]); } }
Form options
| Option | Default | Applies to | Description |
|---|---|---|---|
action_name |
null |
both | Sent to Google, and matched by the constraint when it also sets actionName |
theme |
'light' |
checkbox | light or dark |
size |
'normal' |
checkbox | normal or compact |
challenge is not a form option. Google supports one enterprise.js load per page and its render=
parameter takes one value, so the challenge is a bundle setting and every field on a page shares it.
The Twig theme is prepended automatically. It emits no JavaScript: the field carries data-codein-recaptcha
attributes, and the shipped asset acts on them. In the score challenge it calls grecaptcha.enterprise.execute
on submit, fills the hidden field and resubmits with requestSubmit(), which preserves the clicked button
and runs the other submit listeners. In the checkbox challenge the token is written into the hidden field by
the widget callback as soon as the visitor solves it.
To restyle one challenge without touching the other, override the recaptcha_enterprise_score_widget
or recaptcha_enterprise_checkbox_widget block rather than recaptcha_enterprise_widget, which only
dispatches between them. Keep the data-codein-recaptcha attributes on the input and, for checkbox,
the data-codein-recaptcha-container div: they are how the asset finds the field.
Loading the scripts
The bundle never loads Google's script — the application does. The bundle cannot know whether the visitor consented to Google, and a script placed on the page without consent is the application's liability, so this is deliberately not configurable: a flag would still ship a default that loads it.
"Adding the scripts to your layout" gives the minimum; this is the full contract. Add the loader once per page,
after consent, with onload=codeinRecaptchaOnload, alongside the bundle's own asset:
{# score: the site key is bound to the loader at load time #} <script src="https://www.google.com/recaptcha/enterprise.js?render={{ site_key }}&hl=fr&onload=codeinRecaptchaOnload" async defer></script> {# checkbox: the widgets are rendered explicitly, one per field #} <script src="https://www.google.com/recaptcha/enterprise.js?render=explicit&hl=fr&onload=codeinRecaptchaOnload" async defer></script> {# both challenges: the bundle's submission handling, no consent needed #} <script src="{{ asset('bundles/codeinrecaptchaenterprise/recaptcha-enterprise.js') }}" defer></script>
The two tags are not interchangeable: the render= value follows the challenge setting, and a page must never
carry both — which the single site_key already prevents. hl= is yours to set, and omitting it lets Google
detect the language from the browser.
codeinRecaptchaOnload is public API. It is the only supported readiness signal: grecaptcha.enterprise.ready()
does not queue callbacks registered before the library exists, so the asset queues everything itself and drains
the queue when the callback fires. Nothing depends on the two scripts landing in a given order — a library that is
already there is detected directly, and a callback that fired before the asset ran is caught by a short poll.
Any number of fields of the configured challenge can then appear on one page: several score fields share the single bound key, several checkbox fields each render into their own container. A visitor who submits before the library has landed is safe — the submission is held and replayed, rather than throwing and leaving the form silently dead.
The asset also exposes window.codeinRecaptcha:
| Member | Purpose |
|---|---|
refresh(root) |
Wire up fields added after load — Turbo, Stimulus, an AJAX-loaded modal. Idempotent, so calling it on the whole document again is safe. |
whenReady(callback) |
Run a callback once grecaptcha.enterprise exists, for application code of your own. |
// after injecting a form into the page window.codeinRecaptcha.refresh(modal);
⚠️ GDPR: with no loader there is no token, so the constraint refuses every submission and the visitor is locked out of the form.
on_error: allowdoes not rescue this — it covers an unreachable Google, while a missing token is a legitimateMISSINGrefusal. An application that omits the script until consent is given must also skip the constraint until then, with a validation group or by not adding the field at all.
When the token cannot be fetched
If grecaptcha.enterprise.execute() rejects, or the loader never arrives within ten seconds — blocked, offline,
or held back by a consent manager — the bundle dispatches a cancelable codein-recaptcha:error event on the form
and then submits with an empty token, which the server refuses as MISSING. Cancel the event to keep
the submission blocked and handle it yourself:
form.addEventListener('codein-recaptcha:error', function (event) { event.preventDefault(); // the form stays unsubmitted; show your own message });
Submitting an empty token is deliberate: it is a refusal the application already reports through the constraint, whereas a form left in a prevented state gives the visitor nothing at all.
Showing the error
Render the field with form_row(), or let form_widget(form) render the whole form. Either way the theme's
recaptcha_enterprise_row block emits the violation above the field:
{{ form_row(form.recaptchaToken) }}
Calling form_widget(form.recaptchaToken) on its own renders the field without the error, as with any
Symfony field. The field inherits from HiddenType, whose hidden_row block renders the widget alone — so
without the bundle's own row block the visitor would be refused with no message at all. Errors do not reach
form_errors(form) either: HiddenType passes them to the parent, and the type sets error_bubbling back
to false so the message stays beside the widget.
Set error_bubbling: true on the field if you would rather collect the message in the form-level summary.
Handling a failed captcha
The message the visitor sees is deliberately vague. Everything the application needs to react — the score, the reason Google gave, whether Google answered at all — is attached to the violation, and the finder reads it back:
use Codein\RecaptchaEnterpriseBundle\CaptchaFailure\FinderInterface; public function __construct(private readonly FinderInterface $captchaFailures) {} // ... if ($form->isSubmitted() && !$form->isValid() && $this->captchaFailures->has($form)) { $failure = $this->captchaFailures->get($form); // react to $failure, then re-render the form }
has() answers the question, get() returns the failure and throws a NoFailureException when there is none —
there is no null to guard against. Pass the form you validated: the whole tree is searched, so the captcha field
is never named and renaming it breaks nothing. Passing the field itself works too.
A failure is exactly one of three:
| Meaning | getScore() |
getInvalidReason() |
|
|---|---|---|---|
isInvalidToken() |
No token, or Google refused the one submitted | null |
The reason, e.g. EXPIRED |
isLowScore() |
Genuine token, risk analysis below the threshold | The score | null |
isUnavailable() |
Google could not be asked at all | null |
null |
isUnavailable() never fires under on_error: allow, which refuses nothing. The raw assessment stays available
as $failure->result or $failure->getResult(), and the violation as $failure->violation
or $failure->getViolation().
The finder never looks at the challenge — it reads what the validator recorded, and that validator has a single code path — but the two challenges do not fail the same way, so what is worth handling differs.
With the score challenge
All three outcomes are reachable. The interesting one is the low score: the token was genuine and Google
assessed it, the risk analysis simply stayed under min_score. There is nothing the visitor can do about it, so
the useful reaction is on your side — log it, raise a flag, ask for a second factor:
if ($failure->isLowScore()) { $logger->warning('reCAPTCHA refused {score} on {action}', [ 'score' => $failure->getScore(), 'action' => $failure->result->action, ]); } elseif ($failure->isUnavailable()) { $this->addFlash('warning', 'The captcha service is unreachable, please try again in a moment.'); }
The failure then carries a full assessment — note that success and valid are both true, which is what
separates a low score from a refused token:
violation code = LOW_SCORE_ERROR, parameters {{ reason }} = "NONE", {{ score }} = "0.1"
result success = true, valid = true, action = "contact", score = 0.1,
invalidReason = null, error = null, raw = [the whole payload]
$failure->result->raw holds Google's untouched answer, so the risk analysis reasons are there for a log
line even though the bundle does not model them.
A low score is also where the hybrid pattern belongs. Rather than refusing outright, an application can step
the visitor up to a check of its own — an e-mail with a validation link, a moderation queue, a delayed publication.
Call VerifierInterface::verify() yourself for that, and do not also attach the constraint: the second
assessment of the same token comes back as DUPE.
With the checkbox challenge
With min_score: 0 the score is never held to a threshold, so isLowScore() never fires, and a refused token
carries no risk analysis, so getScore() stays null — a property of the refusal rather than of checkbox keys,
which Google scores like any other. What to handle instead is that refused token, since the checkbox is only
valid for about two minutes:
if ($failure->isInvalidToken()) { $this->addFlash('warning', match ($failure->getInvalidReason()) { InvalidReason::EXPIRED => 'Please tick the checkbox again, it expired.', InvalidReason::MISSING => 'Please tick the checkbox.', default => 'The captcha could not be verified, please try again.', }); }
Both reasons produce the same failure shape, but not the same story: MISSING never reached Google at all —
the field was empty, so raw is empty too — whereas EXPIRED comes back from a real assessment:
violation code = INVALID_TOKEN_ERROR, parameters {{ reason }} = "EXPIRED", {{ score }} = "null"
result success = false, valid = false, action = null, score = null,
invalidReason = InvalidReason::EXPIRED, error = null, raw = [the whole payload]
One trap: isLowScore() is unreachable here only because min_score is 0. Leave the default 0.5 on
a checkbox key and a visitor who ticked the box is still refused whenever Google scores the interaction
below the threshold — with no second attempt to offer, since the challenge was already passed. Raise
the threshold above 0 only if the application answers a low score with something other than a refusal.
Verification result
VerifierInterface can be used outside the validator. It is stateless: the verdict is the returned Result, never
something read back from the service afterwards.
use Codein\RecaptchaEnterpriseBundle\Verifier\VerifierInterface; public function __construct(private readonly VerifierInterface $verifier) {} // ... $result = $this->verifier->verify($token, 'contact'); $result->success; // whether the token may be accepted, score aside $result->valid; // what Google said about the token itself $result->score; // null when the assessment carried no risk analysis $result->invalidReason; // an InvalidReason enum case, or null $result->getInvalidReasonName(); // e.g. "EXPIRED" $result->error; // set only when no assessment could be obtained at all $result->raw; // the untouched payload
Inside a validator, the same Result is attached to the violation as its cause, so
$violation->getCause() reaches it without calling the verifier again. From a controller, reach for
the finder described in "Handling a failed captcha" rather than unwrapping the form errors by hand.
Two shapes are easy to misread. An empty token never reaches Google: verify('') answers MISSING without
an HTTP call. And success === true together with a non-null error is an accepted outage under
on_error: allow — there is no score to judge, so treat it as a case to step up rather than as a pass.
When Google cannot be reached
A network failure, a rate limit or a Google outage says nothing about the token, so the bundle treats it as its own
outcome instead of reporting a valid token as invalid. on_error decides what happens then:
| Value | Behaviour |
|---|---|
deny (default) |
The submission is refused. Safe, but a Google outage blocks every form. |
allow |
The submission passes without an assessment. Keeps forms working, and lets bots through while the outage lasts. |
Either way the failure is logged at error level, and the violation raised by deny carries
the RecaptchaEnterprise::UNAVAILABLE_ERROR code so it can be told apart from a genuinely refused token.
What the bundle sends to Google
Beyond the token and the site key, the assessment event carries whatever the current request can supply. All of it is optional to Google and omitted when empty:
| Field | Source | Why it matters |
|---|---|---|
expectedAction |
the action_name form option |
Rejects a token minted for another action |
userIpAddress |
Request::getClientIp() |
Feeds IP reputation into the risk analysis |
userAgent |
the User-Agent header |
Feeds device signals into the risk analysis |
requestedUri |
Request::getUri() |
Tells Google which page triggered the assessment |
⚠️ Configure
framework.trusted_proxies. Behind a reverse proxy, a load balancer or a Docker network,getClientIp()returns the proxy's address, so Google scores every visitor from a single internal IP and the risk analysis degrades as traffic grows. A private address such as10.x,172.16-31.xor192.168.xinevent.userIpAddressis the symptom.
requestedUri is the full URI including the query string. If your form pages carry anything sensitive there,
that value reaches Google.
Architecture
Verification is split in two, so that deciding and talking to Google never mix:
Verifierholds every decision: the empty token short circuit, the expected action check, the score-free outage policy. It knows nothing about HTTP.GatewayInterfaceis the port to Google. An implementation only translates the wire format into anAssessmentvalue object, and raises a domain exception when there is no assessment to translate.
HttpGateway is the one implementation, calling the REST Assessments API through Symfony's HTTP client.
The official google/cloud-recaptcha-enterprise SDK is deliberately not used: the bundle makes a single unary call,
for which the SDK adds only a protobuf and gRPC stack, and its google/gax dependency requires
ramsey/uuid ^4, which cannot be installed alongside applications held at ramsey/uuid 3.x — Ibexa 4.6 among
them. Another gateway can be added behind the port without the domain noticing.
The gateway throws, rather than returning a failed assessment, whenever Google did not answer with one:
| Exception | Cause |
|---|---|
TransportException |
Network failure, undecodable body, rate limit (429) or server error (5xx) — all transient |
AuthenticationException |
401 or 403: a missing, wrong or unauthorised API key |
InvalidRequestException |
400 or 404: an unknown project or a malformed event |
All three implement AssessmentExceptionInterface. Verifier catches it, so no exception ever escapes into form
validation.
The split holds on the way back too: the CaptchaFailure\Finder only reads what the validator recorded on
the violation. It decides nothing, which is why it needs no configuration and no dependencies.
Development
Everything runs in Docker, so no local PHP or Composer is needed:
$ make install # build the image and install the bundle and tool dependencies $ make test # run the test suite $ make phpstan # run the static analysis (level 9) $ make cs # check the coding standards $ make cs-fix # fix the coding standards $ make qa # run all of the above
The default stack is the lowest supported one, PHP 8.1 with --prefer-lowest --prefer-stable, which is what proves
the declared requirements hold. Override it to work against a newer stack:
$ make update-latest PHP_VERSION=8.4
$ make test PHP_VERSION=8.4
composer.lock is not committed. This is a library, so consumers resolve their own dependency versions and a committed
lock file would only mislead the matrix build.
Issues and pull requests are welcome on Codein-Labs/recaptcha-enterprise-bundle.
Upgrading from artack/recaptcha-enterprise-bundle:0.2.0
Start with the rename. Every identifier carrying the old vendor changed, and nothing else in this table has any behaviour attached to it:
| What | Old | New |
|---|---|---|
| Composer package | artack/recaptcha-enterprise-bundle |
codein/recaptcha-enterprise-bundle |
| PHP namespace | Artack\RecaptchaEnterpriseBundle\ |
Codein\RecaptchaEnterpriseBundle\ |
| Bundle class | ArtackRecaptchaEnterpriseBundle |
CodeinRecaptchaEnterpriseBundle |
| Configuration key | artack_recaptcha_enterprise |
codein_recaptcha_enterprise |
| Configuration file | config/packages/artack_recaptcha_enterprise.yaml |
config/packages/codein_recaptcha_enterprise.yaml |
| Environment variables | ARTACK_RECAPTCHA_ENTERPRISE_* |
CODEIN_RECAPTCHA_ENTERPRISE_* |
| Published asset | bundles/artackrecaptchaenterprise/… |
bundles/codeinrecaptchaenterprise/… |
| Readiness callback | artackRecaptchaOnload |
codeinRecaptchaOnload |
| JavaScript global | window.artackRecaptcha |
window.codeinRecaptcha |
| DOM attributes | data-artack-recaptcha* |
data-codein-recaptcha* |
| Failure event | artack-recaptcha:error |
codein-recaptcha:error |
| Twig theme namespace | @ArtackRecaptchaEnterprise |
@CodeinRecaptchaEnterprise |
Then the changes a rename does not cover. "What changed from artack/recaptcha-enterprise-bundle" explains why each of these moved; this is the checklist:
- Add the script tags to your layout. The bundle no longer loads
enterprise.js, and it no longer emits any inline JavaScript. Without both tags every submission is refused asMISSING. See "Adding the scripts to your layout". This is the one step that cannot be skipped. - Run
assets:install. The submission handling is a published asset now. - Drop any wiring around
Artack\RecaptchaEnterpriseBundle\Service\.IpResolver,IpResolverInterface,UserAgentResolverandUserAgentResolverInterfaceare gone; the verifier reads the request stack directly. - Replace
VerifierInterface::getLatestResult()with theResultthatverify()returns, or with$violation->getCause()inside a validator. - Rebuild any hand-constructed
Verifier. It takes aGatewayInterfaceinstead of the project id, site key, API key and the two resolvers. Code going through the container is unaffected. - Rebuild any hand-constructed
Result.$rawmoved in the constructor and$invalidReason,$errorandgetInvalidReasonName()were added;$invalidReasonis anInvalidReasonenum case. Use named arguments. - Rename
InvalidReason::fromName()tofromApiValue(), which is what it always did. - Remove the
locale,script_csp_nonceand per-fieldchallengeform options.hl=now goes on your own script tag, there is no inline script left to nonce, andchallengeis a bundle setting. - Drop any custom row block you added to make the violation appear; the theme defines
recaptcha_enterprise_rowitself. Seterror_bubbling: trueon the field if you relied on the message reachingform_errors(form). - Review
min_score. The score check fails closed now: an assessment with no risk analysis is refused rather than passed.min_score: 0restores the old behaviour. - Pass
message:to the constraint to keep the old default wording,You may be sending automated requests.
Nothing else in an existing integration needs to change. RecaptchaEnterpriseType, the RecaptchaEnterprise
constraint and VerifierInterface::verify() keep their names and their signatures.
License
This bundle is released under the MIT License.