Weave Code
Weaver
Helps Laravel developers discover, compare, and choose open-source packages. See popularity, security, maintainers, and scores at a glance to make better decisions.
Feedback
Share your thoughts, report bugs, or suggest improvements.
Subject
Message

Captcha Laravel Package

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.

View on GitHub
Deep Wiki
Context7

Getting Started

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.

Implementation Patterns

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.

Gotchas and Tips

  • The session is the source of truth. 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.
  • Fonts in containers. "Could not find/open font" or a captcha that renders as lines only almost always means GD was compiled without FreeType, or the packaged font path isn't readable. Verify with a one-line 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.
  • Never cache /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.
  • Unknown preset names. Passing a config key that doesn't exist falls back to the default (or throws, depending on version) — typo'd preset names are a common "why is the captcha the wrong size" bug. Validate the key before it hits a URL.
  • Case sensitivity is controlled by 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.
  • Accessibility. There's no audio alternative, and image-only challenges fail WCAG. Provide a fallback (honeypot + rate limit) and always render visible error text next to the field, not just a red border.
  • Extension points are thin by design. Beyond presets in 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.
  • Upgrade watch. The 2.x → 3.x line changed the facade/config surface (preset keys, API route response, rule signature). Pin the major version and re-read config/captcha.php after composer update — published configs don't auto-merge, so new keys silently default.
Weaver

How can I help you explore Laravel packages today?

Conversation history is not saved when not logged in.
Prompt
Add packages to context
No packages found.
jinas/dhivatar
store-doc/laravel-storage-driver
plus-it/laravel-translation-manager
diephp/perhaps
diephp/telegraph
osmaviation/spreadsheet
microsoft/fluentui-system-icons
mmccook/php-json-canonicalization-scheme
middlewares/firewall
qubiqx/montapacking
nyoncode/laravel-package-console
pnscripts/filament-broadquill-lite
pnscripts/laravel-sentfold-lite
vleap/mx-sdk-laravel-internal
twidpay/utils
htetmyathlaing/scout-elasticsearch-driver
artisan-toolbox/front-awesome
ls/client-assistant
users-au/laravel-client
cgv/localized-resource