mews/captcha
mews/captcha is a Laravel CAPTCHA package offering image and audio challenges, built-in validation rules, customizable styles, and simple helpers to protect forms from spam and bots.
Install, publish the config, and confirm the image route responds before touching any form:
composer require mews/captcha
php artisan vendor:publish --provider="Mews\Captcha\CaptchaServiceProvider"
php artisan route:list | grep captcha # captcha/{config?} and captcha/api/{config?}
The package ships its own TTF fonts and builds images with Gregwar/Captcha, so the one hard requirement is GD with FreeType:
php -m | grep -i gd
# Docker/Alpine: docker-php-ext-install -j$(nproc) gd (build with --with-freetype)
config/captcha.php holds named presets — default, flat, mini, inverse, math (verify the exact list in your installed version). Each preset carries length, width, height, quality, math, expire, bgImage, bgColor, fontColors, chars, and sensitive.
First use case — a public contact form. Two lines of Blade plus one rule:
<form method="POST" action="{{ route('contact.store') }}">
@csrf
<img id="captcha-img" src="{{ captcha_src('flat') }}" alt="Verification code">
<button type="button" onclick="document.getElementById('captcha-img').src='{{ captcha_src('flat') }}?'+Date.now()">↻</button>
<input type="text" name="captcha" required autocomplete="off" inputmode="text">
@error('captcha') <span class="error">{{ $message }}</span> @enderror
</form>
// app/Http/Requests/ContactRequest.php
public function rules(): array
{
return [
'name' => ['required', 'string', 'max:100'],
'email' => ['required', 'email'],
'message' => ['required', 'string', 'max:2000'],
'captcha' => ['required', 'captcha'], // rule provided by the package
];
}
public function messages(): array
{
return ['captcha.captcha' => 'The verification code is incorrect.'];
}
That's the 90% path: the captcha route renders and stores the phrase in the session/cache; the captcha validation rule compares the submitted value against it. Reach for the facade (Captcha::create(), Captcha::img(), Captcha::src(), Captcha::check()) only when you need to control rendering or storage yourself.
Refresh without a full page reload. Always append a cache-buster; browser and CDN proxies will happily serve a stale image otherwise.
const img = document.querySelector('#captcha-img');
img.addEventListener('click', () => {
img.src = `${img.dataset.src}?${Date.now()}`; // data-src = captcha_src('flat')
});
Never hardcode /captcha/flat — use captcha_src() so subdirectory deploys and APP_URL changes keep working.
Named presets per form. Add your own keys instead of mutating default, then pass the key through captcha_src('login') and Captcha::create('login'). Keep the login preset short and the registration one longer; 'math' => true gives an arithmetic challenge that's noticeably friendlier on mobile.
Multiple forms / multiple tabs. Session-backed captchas are "last generated wins" — rendering a second image invalidates the first, and a user with two open tabs will fail submission unpredictably. For multi-form pages use the API route, which returns a keyed response so each image is independently verifiable:
const { key, img } = await fetch('/captcha/api/default', { credentials: 'same-origin' }).then(r => r.json());
// render `img` (inline data URI), keep `key` in a hidden input
// POST { captcha: value, key }
The captcha validation rule resolves the posted key internally, so the server-side rule is unchanged. Confirm the exact response shape against your installed version with curl -s /captcha/api/default | jq.
Custom rendering / storage. When you need the raw builder (inline base64 in an email, save to disk for a queued job, custom <img> attributes):
$builder = Captcha::create('default'); // returns the Gregwar builder
$inline = $builder->inline(); // data:image/jpeg;base64,...
$html = Captcha::img('flat'); // full <img> tag
$ok = Captcha::check($request->input('captcha'));
Treat Captcha::check() as single-use: it consumes the stored value, so don't call it in a middleware and again in the validator.
Integration tip — FormRequest plus throttle. Captcha is a speed bump, not a firewall. Pair it with throttle on the POST route and on the image route itself:
Route::get('/captcha/{config?}', ...)->middleware('throttle:30,1'); // or in config middleware
Route::post('/contact', [ContactController::class, 'store'])->middleware('throttle:5,1');
Testing pattern. Mocking the Captcha facade is not enough — the validator uses the container instance registered by the service provider. Override the rule in the test environment:
// app/Providers/AppServiceProvider::boot()
if ($this->app->environment('testing')) {
Validator::extend('captcha', fn () => true); // replaces the package's extension
}
Alternatively bind a stub: $this->instance('captcha', $stub) in setUp(). Avoid the "fetch the image, read the session, post the answer" test — it's brittle and couples your suite to internals.
SESSION_DRIVER=array (or a token-only API client with no cookies) means validation always fails. For stateless SPAs, use captcha/api/*, return the key to the client, and keep the same session/cookie middleware group on the endpoint.middleware config matters. Package routes are registered under web by default; if you change it, you'll lose the session and CSRF context the route depends on. Behind a proxy or in a subdirectory, also check TrustProxies and that captcha_src() emits the right host.imagettftext() smoke test rather than debugging the package.bgImage breaks silently-ish. Paths must resolve at runtime (public_path('images/captcha-bg.jpg')), be readable by the web user, and be a supported format. Ship a fallback preset without a background image for local/dev./captcha/*. Add Cache-Control: no-store via middleware or your reverse proxy; the package's quality setting makes JPEGs look cacheable and they will be.sensitive. Leave it off unless you have a reason; users type in caps on mobile.expire is a time window, not a one-shot guarantee — but successful validation still clears the stored phrase. Don't reuse the same captcha for a "resend code" flow.captcha.php, custom fonts, and swapping the renderer binding, there isn't much to hook. If you need sliding puzzles, hCaptcha, or Turnstile, wrap those behind your own Captcha-like interface now so swapping later is a one-line change in the FormRequest.config/captcha.php after composer update — published configs don't auto-merge, so new keys silently default.How can I help you explore Laravel packages today?