arcanedev/no-captcha
Laravel wrapper for Google reCAPTCHA v2 and v3 that simplifies spam protection. Includes validation rules, Blade directives, helpers, and configurable options so you can secure forms and verify tokens with minimal setup and clean configuration.
Install and publish config
composer require arcanedev/no-captcha
php artisan vendor:publish --provider="Arcanedev\NoCaptcha\NoCaptchaServiceProvider"
The service provider is auto-discovered; the NoCaptcha facade alias is registered for you. The published config/no-captcha.php is the first file to open — it holds secret_key, site_key, widget options, skip_ip, and error_message.
Get keys before touching code
Create a reCAPTCHA v2 site in the Google reCAPTCHA admin console (choose "reCAPTCHA v2 / I'm not a robot" or "Invisible reCAPTCHA badge"). Copy the site key and secret key into .env:
NOCAPTCHA_SITEKEY=your-site-key
NOCAPTCHA_SECRET=your-secret-key
(Confirm the exact env names in the published config — older docs sometimes use RECAPTCHA_*.)
First use case: a contact form
<form method="POST" action="/contact">
@csrf
<input name="email" type="email">
<textarea name="message"></textarea>
{!! NoCaptcha::renderJs() !!}
{!! NoCaptcha::display() !!}
<button type="submit">Send</button>
</form>
$request->validate([
'email' => 'required|email',
'message' => 'required',
'g-recaptcha-response' => 'required|captcha',
]);
That is the entire happy path: render the widget, submit the token in g-recaptcha-response, and let the package's captcha validation rule verify it against Google.
Where to look first when something is off
config/no-captcha.php — keys, skip_ip, options.NoCaptcha facade methods (display, renderJs, displaySubmit, verifyResponse, setLang).captcha validator rule (source: src/NoCaptchaServiceProvider.php / src/Validator.php).Blade partial for consistent rendering
Wrap the widget so every form uses the same options and only loads the JS once:
{{-- resources/views/partials/captcha.blade.php --}}
@once
{!! NoCaptcha::renderJs(app()->getLocale()) !!}
@endonce
{!! NoCaptcha::display() !!}
@once prevents duplicate <script src="...recaptcha/api.js"> tags when several forms are on one page.
Invisible reCAPTCHA (bind to form submit)
Use invisible v2 when you don't want a checkbox. Configure options in config, then use displaySubmit:
{!! NoCaptcha::renderJs() !!}
{!! NoCaptcha::displaySubmit('contact-form', 'Send') !!}
displaySubmit($formId, $buttonText) renders the invisible widget plus the callback wiring that submits #contact-form after the challenge succeeds. The form must still include g-recaptcha-response server-side validation.
AJAX / JSON API submissions
When the form is submitted by fetch/axios, verify manually instead of using the rule:
use Arcanedev\NoCaptcha\Facades\NoCaptcha;
$response = NoCaptcha::verifyResponse(
$request->input('g-recaptcha-response'),
$request->ip()
);
if (! $response->isSuccess()) {
return response()->json(['error' => 'Captcha failed'], 422);
}
This is the escape hatch for SPA front-ends (Vue/React/Livewire) where the Blade rule doesn't fit.
Localization of the widget
{!! NoCaptcha::renderJs('fr') !!}
{{-- or globally --}}
@php NoCaptcha::setLang('fr'); @endphp
Google renders the challenge in the page language only if you pass it; otherwise it detects from the browser.
Reusable Form Request
Move the rule out of the controller:
class ContactRequest extends FormRequest
{
public function rules(): array
{
return [
'email' => 'required|email',
'g-recaptcha-response' => 'required|captcha',
];
}
}
Keeps the controller clean and centralizes the captcha requirement.
Testing pattern (avoid hitting Google)
Bind a fake so the captcha rule always passes in tests, or disable the widget for the testing environment:
// tests/TestCase.php or a dedicated provider
$this->app->bind(\Arcanedev\NoCaptcha\Contracts\NoCaptcha::class, function () {
return new class implements \Arcanedev\NoCaptcha\Contracts\NoCaptcha {
public function verifyResponse($response, $clientIp = null) {
return new class {
public function isSuccess() { return true; }
};
}
// ...other interface stubs
};
});
Simpler alternative: add the test runner's IP (127.0.0.1) to skip_ip in config/no-captcha.php only under APP_ENV=testing, or conditionally disable validation in the Form Request.
Multiple widgets on one page
If you have more than one form, each display() call generates its own g-recaptcha div. Ensure you only call renderJs() once (@once), and give each widget a unique callback if you're using invisible mode — Google's JS binds by data attributes, so duplicate callbacks will collide.
reCAPTCHA v3 is not supported. This package wraps the v2 API (checkbox and invisible). There is no built-in score/action handling, no execute() helper. If you need v3 you'll call Google's siteverify endpoint yourself (or use a dedicated v3 package) — NoCaptcha::verifyResponse() will still verify the token, but you must interpret the returned score yourself. For new projects, weigh v2 vs v3 before adopting this package.
Package age and Laravel version constraints. Last release was Feb 2022. Check the composer.json constraint against your Laravel version; you may need to bump constraints locally or fork. Auto-discovery and the facade generally keep working across Laravel versions, but don't assume Laravel 11 is officially supported.
Tokens are single-use and expire (~2 minutes). Never cache or reuse g-recaptcha-response. If you verify manually, verify at the start of the request; on failure, re-render the widget. A stale token produces a confusing validation error on a slow form.
hideBadge has legal strings attached. If you set hideBadge => true, Google's terms require you to display the reCAPTCHA attribution text ("This site is protected by reCAPTCHA and the Google Privacy Policy and Terms of Service apply") somewhere visible. The package won't do it for you.
Empty or placeholder keys fail silently / confusingly. If NOCAPTCHA_SECRET is blank, verifyResponse returns a failure rather than throwing a friendly exception. Add a boot-time check in a service provider or a config sanity assertion in non-production environments.
Config caching. After changing .env or config/no-captcha.php, run php artisan config:clear. In production, config:cache is fine but you must redeploy (or clear) when keys rotate.
Domain registration. Google validates the host it's embedded on. If you test on localhost but deploy to staging.example.com, that host must be added in the reCAPTCHA console, or you get a "invalid domain for site key" error in the browser console — not a server-side validation error, so check the browser first.
Rule name collisions. The rule is registered as captcha. Another package using the same rule name will overwrite it; the symptom is "captcha validation always passes" or "class not found" in the validator. Inspect Validator::extend calls if behavior seems wrong.
Local development. Don't disable captcha by commenting out the rule — use skip_ip in config (e.g. ['127.0.0.1', '::1']) or conditionally strip the rule in a Form Request when app()->environment('local'). This keeps staging/production honest.
Script duplication. Calling NoCaptcha::renderJs() inside a form partial included by multiple forms injects the Google script more than once. Use @once or move renderJs() to your layout <head>.
Error message customization. Default error text lives in config('no-captcha.error_message') (with a :attribute placeholder). Override there rather than in every Form Request.
Manual verification for APIs returns a response object. verifyResponse() returns a wrapper — check isSuccess() and, on failure, log the underlying getErrorCodes() so you can distinguish "invalid keys" (a config/deployment bug) from "user failed the challenge" (a user problem). That distinction saves hours during launches.
How can I help you explore Laravel packages today?