gregwar/captcha-bundle
GregwarCaptchaBundle adds a captcha form type to Symfony forms using gregwar/captcha. It renders a configurable captcha image, embedded by default, with options for width, height, and more. Supports Symfony 6–8 and PHP 8+.
Architecture fit — the headline finding. gregwar/captcha-bundle is a Symfony Form Component bundle, not a Laravel package. It ships a CaptchaType that plugs into Symfony's form builder, a Twig form theme block, and Symfony bundle auto-registration via Flex. None of that has a counterpart in Laravel's validation/Blade architecture. The adoptable asset for a Laravel stack is the underlying standalone library, gregwar/captcha, which is framework-agnostic PHP and does the actual work (GD-based image generation, phrase storage handoff). Everything the bundle adds on top — the form type, the Twig widget, the as_url route, the YAML config tree — must be rebuilt in Laravel idioms. This distinction should be settled before an estimate is written; conflating the two will produce a badly wrong estimate in the optimistic direction.
What the bundle tells us about the library. The bundle's option surface is effectively the library's capability inventory, and it is a useful spec even if we never install the bundle: width/height/length/quality/charset/font, distortion/max_front_lines/max_behind_lines/background_color/background_images/interpolation/ignore_all_effects (difficulty tuning), keep_value (code stability across refresh), session_key (multiple challenges per page), invalid_message (translatable copy), disabled (per-environment kill switch), humanity (only 1-in-N forms gated after a success), and bypass_code (always-valid escape hatch). We want most of these knobs in our Laravel wrapper; the bundle's config schema is the requirements document.
Integration feasibility. Moderate and well-bounded. The library is small, MIT, dependency-light, and requires only the gd extension. The build is: an image-serving route, server-side phrase storage with TTL, a validator rule, and a Blade component. That is a one-to-two-week internal package, not a composer require. The risk is not in the library; it is in the glue we own forever.
Technical risk — ranked.
bypass_code is a footgun. A single always-valid string, configured the same way in every environment, is a backdoor if it ever leaks into production config or a repo. If we expose it, it must be env-scoped, secret-managed, and audited.Key questions for engineering before approval.
gd extension, or is that a Dockerfile change and a redeploy risk on slim/alpine base images?as_url complexity at all — in Laravel we likely do not, because Laravel's session abstraction already solves it and does so more cleanly than the bundle's whitelist_key session allowlist.Stack fit — state it plainly. Do not install gregwar/captcha-bundle in a Laravel application. There is no path: no bundle registration, no form type system, no Twig form theme contract, no Symfony DI config tree. The correct artifact is composer require gregwar/captcha plus a first-party wrapper. Any plan that says "add the bundle" is wrong and should be corrected in the ticket.
Target shape of the internal wrapper (proposed, ~1.5 sprints with tests):
config/captcha.php mirroring the bundle's option surface — width, height, length, quality, charset, font, distortion, invalid_message, disabled, humanity, session_key, bypass_code, ttl. Same semantics, Laravel config idioms, env-driven defaults so dev/staging run disabled=true and QA is never blocked.CaptchaService that generates a phrase, renders a PNG via the library, and stores hash_equals-comparable state in the session (or cache) under a namespaced key with a TTL./captcha/{key}) returning image/png with Cache-Control: no-store. Serve from a route, not files — this deliberately avoids the library's as_file/expiration/gc_freq garbage-collection machinery and its associated failure mode (GC deleting an in-flight image). No filesystem writes, no permissions surface, no cron.<x-captcha /> rendering the image, a labelled text input, and an accessible reload affordance.captcha that compares case-insensitively, consumes the phrase on both success and failure (single-use; prevents replay and brute force), and returns a translated invalid_message.CaptchaDriver) with gregwar as the first implementation, so hCaptcha/Turnstile/honeypot-only can be swapped in without touching call sites. Given the efficacy ceiling above, this interface is the most valuable thing we build.Migration path. If a third-party CAPTCHA is already in place, run both in parallel behind the driver interface for one release: keep the existing control on the highest-risk form, add gregwar on a low-risk form, compare. If greenfield, ship rate limiting + honeypot first (cheap, accessible, no vendor), and add the image challenge only where those prove insufficient. If we are on Symfony somewhere in the estate, the bundle itself is a legitimate two-line install there — that is the one context where the headline promise holds.
Compatibility notes to check off.
gd extension; ext-gd must be in composer.json and in the container image.bypass_code into production config without a secrets manager and an env guard that hard-fails if set outside local/testing.Sequencing. (1) One-day spike: install library, prove GD renders in the actual container, prove session round-trip. (2) Build the wrapper with unit tests around the validator (replay, expiry, case sensitivity, hash_equals). (3) Pilot on exactly one low-traffic, high-spam form — contact/support is the ideal candidate, it is lowest-stakes. (4) Instrument challenge-shown, challenge-passed, challenge-failed, and form-abandoned counts. (5) Decide on expansion only after 2–4 weeks of data. (6) Resolve the accessibility alternative before touching signup, login, or password reset.
Maintenance. Single-maintainer project with recent releases that are mostly dependency bumps and community PRs, some automated. Bus factor is the dominant concern: if the maintainer stops, we own a vendored fork of an image-generation library. Mitigation is cheap — pin the version, keep the wrapper thin so the library is replaceable, and avoid letting library-specific types leak into application code. MIT license means forking is legally unproblematic; the cost is engineering time, not legal review. Budget roughly 2–4 hours per quarter for dependency review and a PHP-version bump check against new Laravel releases.
Support. Community-only, no SLA, no vendor to escalate to. Documentation is good for Symfony and nonexistent for Laravel — the wrapper's README is our real documentation and must be written as though the author leaves. Plan for the "why does the captcha fail on one node" question to land on whoever owns the wrapper; make the answer findable.
Scaling. Image generation is CPU-bound GD work at roughly single-digit milliseconds per image. That is negligible relative to almost any PHP request, but it is unbounded if a form is a bot target — a spam wave increases the resource we spend generating challenges for the bots we are trying to block, which is a mild denial-of-service amplifier. The route must therefore be rate-limited per IP independent of the form, and the challenge endpoint should be cheap to reject. No CDN benefit: images are per-session and uncacheable by design. Multi-node is handled by the shared session store, not by the bundle's URL-serving mode.
Failure modes to design against.
gd → 500 on the captcha route. Fail at deploy in CI, not in production.bypass_code configured in production → silent, total bypass. Guard in the service provider.quality=30 default, thin fonts, aggressive distortion, dark backgrounds) → legitimate users churn and support tickets rise. Log pass/fail rates per form; a passing rate far below ~90% means the difficulty knobs are wrong for our audience./captcha/* with an existing application route.Ramp-up. Low for anyone who has shipped a Laravel validation rule and a signed/public route — the wrapper is small and conventional. The genuinely time-consuming part is not code, it is the product work: establishing the spam baseline, instrumenting outcomes, and deciding the accessibility strategy. Allocate the engineering estimate to the wrapper and a comparable amount of PM/design time to those three questions, because the wrapper is the easy half.
Bottom line for the roadmap. Approve the library plus a first-party wrapper, behind a driver interface, piloted on one low-stakes form with instrumentation. Reject the bundle for the Laravel estate on compatibility grounds. Treat accessibility as a gating prerequisite for any authentication-adjacent form, not a follow-up ticket. And position the capability honestly in the roadmap as spam-friction reduction — not bot defense, not account security — because the package cannot deliver the latter and the roadmap should not promise it.
How can I help you explore Laravel packages today?