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

mulertech/captcha

MulerTech Captcha is a Laravel package for adding image-based captcha challenges to forms, with validation rules, customizable configuration, and simple Blade integration to help protect your application from bots and spam.

View on GitHub
Deep Wiki
Context7

Getting Started

This is a Symfony bundle, not a Laravel package — there is no service provider, no config/*.php to publish, no Blade views, and no php artisan command. Before you composer require it in a Laravel app, decide which of these you are actually doing:

  1. You run a Symfony app (or a Symfony service alongside Laravel). Straightforward — install and go.
  2. You run Laravel and want this exact captcha. You are signing up to port ~150 lines (GD image generator + session token store + validator) into a Laravel package, or to bridge Symfony Forms into Laravel. Budget half a day, not ten minutes.
  3. You want the idea. Steal the token-per-render + TTL + session-side answer design; it is better than most Laravel captcha packages.

Minimal steps (Symfony path):

composer require <vendor>/math-captcha-bundle:^1.0
php -r "var_dump(extension_loaded('gd'), gd_info()['JPEG Support']);"

Requirements to verify first: PHP 8.4+, ext-gd with JPEG support, Symfony 6.4/7.x/8.x, Forms + Twig. Then add CaptchaType to any form — the form theme and /captcha/refresh route auto-register, so a working captcha is one field addition away.

First use case: drop it on a public contact/registration form that currently has only a honeypot. It is a spam speed bump, not a bot defence — pair it with rate limiting.

Where to look first in the source: CaptchaType (the compound field), the ValidCaptcha constraint + validator, the refresh controller/route, the session token store (TTL + 5-token cap), and the bundled Twig theme (that is where the JS refresh and CSP nonce handling live).

Implementation Patterns

Standard Symfony form usage.

$builder->add('captcha', CaptchaType::class, [
    'csp_nonce' => $nonce, // omit if CspNonceGenerator is auto-detected
])->add('captcha', ValidCaptcha::class); // or via constraints option

With error_bubbling off by default, the error lands on form.captcha.vars.errors — render it next to the image, not in a global error block.

Token lifecycle (the bit worth understanding). Each render mints a unique token, stores {token => answer, expires_at} server-side in the session, and puts only the token in a hidden field. Submit → validator looks up, compares, consumes. Consequences you should design for:

  • Multi-tab is safe by design (unique token per render), which is the main advantage over naive session-key captchas.
  • 5 active tokens per session is a hard cap. A user who refreshes the image repeatedly, or a page that renders the captcha in a modal and a footer, can exhaust it and start getting spurious "expired" errors. Keep to one captcha instance per page.
  • 10-minute TTL means long-lived forms (checkout, long application forms) will fail on submit. Handle the "expired" case distinctly from "wrong answer" — the constraint gives you separate messages, so catch the expired message and auto-refresh rather than telling the user they typed it wrong.

Client-side refresh pattern. On a failed submission you should not re-render the whole page just to get a new captcha image. The bundled theme does a fetch() to /captcha/refresh, gets JSON, and swaps the <img> src plus the hidden token — so the token in the DOM always matches the session state. If you write your own Blade/JS equivalent, replicate exactly that: image and hidden token must be replaced atomically, or you get a guaranteed mismatch on the next submit.

If you are porting to Laravel, the equivalent pieces are:

Symfony bundle Laravel equivalent
Session token store session() or Cache::put("captcha:$token", $answer, 600) — cache is better, it survives session regeneration
/captcha/refresh route Route in routes/web.php, GET, no CSRF, but must be same-origin so the session cookie is sent
Twig form theme Blade partial + a small fetch script
CaptchaType A custom validation rule + a <x-captcha /> component
ValidCaptcha Rule::captcha() or a CaptchaRule object

For the fetch call, set credentials: 'same-origin' explicitly — the default fetch behaviour with cookies is a classic source of "always expired" bugs when the refresh runs from a different subdomain or after a session regeneration on login.

CSP. The theme deliberately uses addEventListener rather than inline onclick, so it survives a strict script-src. Two integration options: let it auto-detect CspNonceGenerator, or pass csp_nonce per field. If you render forms through a cache layer or a fragment cache, the nonce will be stale and the script blocked — exclude any page containing the captcha from full-page caching.

i18n. French and English ship in Symfony Translation format. Laravel's __() will not pick these up; duplicate the four or five strings into lang/ if you port it. Overriding them in Symfony is a translation-file override under the bundle's domain, not a config option.

Gotchas and Tips

  • PHP 8.4 is a hard floor. Most Laravel 10/11 estates run 8.1–8.3; Laravel 12 is fine. Check your runtime matrix before assuming you can install this anywhere.
  • GD with JPEG support is not universal. ext-gd can be present while imagejpeg() is unavailable (minimal Alpine/Docker images, some distro builds). The release notes mention ext-gd but the rendering path is JPEG specifically — verify gd_info()['JPEG Support'] in your container, not just locally.
  • Auto-registered routes can collide. /captcha/refresh is a fixed path with no config knob mentioned. If you already own that path, you get a silent route-order surprise. Check debug:router after install.
  • Auto-registered form theme is convenient but invisible — when the image renders wrong, the first thing to inspect is which theme actually won, not the field config.
  • Session-backed means session-dependent. "Always expired" is almost never a TTL bug. It is: session not persisted across the AJAX call, session regenerated between render and submit, cookie domain mismatch, or a load balancer without sticky sessions. Log the token on render and on validate and diff them before touching anything else.
  • Blank image → GD missing or JPEG-less GD, not a template problem.
  • Errors not showing → error_bubbling is off by design; root-level getErrors() will look empty. Read the field's own errors.
  • ValidCaptcha is a speed bump, not security. A low-quality JPEG with noise and lines stops trivially-scripted OCR and nothing more. Model-based solvers eat this for breakfast. Always combine with rate limiting, a honeypot, and a per-IP submission cap.
  • Accessibility is a real gap. An image-only arithmetic challenge is unusable by screen readers, and under the European Accessibility Act / WCAG 2.2 that is a compliance issue for consumer-facing forms in the EU from mid-2025 onward. Provide an alternative path (honeypot-only, or a text/audio variant) on any form you must keep accessible.
  • No documented config tree. Extension points are: the Twig theme override, translation files, the csp_nonce field option, and the constraint messages. If you need to change TTL (10 min) or the 5-token cap, expect to override services or fork — neither appears configurable in v1.0.0.
  • Single-use tokens are implied but not stated. Do not assume a token can be revalidated for a second submit; render a fresh captcha after every successful submission path that re-displays the form.
  • Version pinning: this is the first release. Pin ^1.0 and read the changelog before 1.1, since the constraint/field API surface is still young.
  • For Laravel specifically: the honest summary is that this package gives a Laravel developer a good design to copy, not a dependency to install. If you do install it via a Symfony Forms bridge, you inherit symfony/form, symfony/http-foundation, symfony/translation, and a Twig pipeline inside a Blade app — that is a large surface area for one form field.
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.
takashato/laravel-vietqr
aitor24/laralang
kraenzle-ritter/entity-relations
dindigital/din-image
pomirleanu/image-colors
sawmainek/laravel-api-generator
delatbabel/jsonresponses
mouhamedfd/permissions-generator
eduplus/qr
laravel2580/idehelpers
delatbabel/viewpages
anavel/gettext
muhamadrezaar/highcharts
delatbabel/keylists
frankkessler/salesforce-laravel-oauth2-rest
anavel/foundation
wdmtech/enviricons
eii/laravel-captcha
muka/shape-reader
tdt/json