webman/captcha
A lightweight CAPTCHA plugin for the Webman PHP framework, offering image-based verification codes with session validation and configurable options. Easily add human verification to Webman routes and forms with minimal setup.
Install with Composer inside the webman project and let the plugin register itself:
composer require webman/captcha
The package is a thin integration layer over gregwar/captcha for webman: it ships a controller that renders a PNG captcha, a route (typically GET /captcha), and a Webman\Captcha\Captcha facade with a static check($code) validator. There is no migration, no service provider to register manually, and no model. If the host runs PHP 8.4, require ^1.0.5 explicitly — earlier tags predate 8.4 support.
First things to inspect, in this order:
config/plugin/webman/captcha/ (and the package's own config/ directory in vendor/webman/captcha/ if the override file was not copied). This is where you choose the storage backend (session vs redis), the code length, image width/height, charset, expiry, and the session/redis key name.src/Captcha.php — this is where the phrase is generated, stored, and compared. It is a few hundred lines; read it once and you own the behavior.Content-Type, and whether cache headers are set.The canonical first use case is a login or registration form. Add the image to your template:
<img src="/captcha" alt="captcha" onclick="this.src='/captcha?t='+Date.now()">
and validate in the controller before you authenticate:
use Webman\Captcha\Captcha;
public function login(Request $request)
{
if (!Captcha::check($request->post('captcha'))) {
return json(['code' => 1, 'msg' => '验证码错误']);
}
// ...authenticate
}
If /captcha returns a 404, the plugin's route was not loaded — check route ordering in config/route.php and confirm the plugin directory exists under config/plugin/.
Storage mode is the first decision. Session mode is the default and is fine for a single-instance deployment. Because webman runs multiple worker processes, you need webman/session (backed by Redis or a shared file store) so the phrase written by one worker is visible to the worker that validates it. If you are behind a load balancer or run more than one host, switch the plugin to its Redis storage mode and make sure the webman/redis pool it references is configured. Session-based captchas behind a sticky-less LB will fail intermittently — that symptom almost always means the storage mode is wrong, not the captcha itself.
Consume the code exactly once. Captcha::check() in most versions compares and returns; it does not necessarily delete the key. Delete or overwrite the stored phrase yourself after a successful check so a captured code cannot be replayed:
if (Captcha::check($code)) {
Session::forget('captcha'); // or the configured key name
}
Wrap validation in a reusable rule. For forms with several entry points, put the check in a request validator or a small middleware so every controller does not re-implement it:
// app/middleware/CaptchaCheck.php
if ($request->method() === 'POST' && !Captcha::check($request->post('captcha'))) {
return response('captcha invalid', 422);
}
Apply it selectively via route middleware, not globally — you do not want it firing on asset or API routes.
SPA / JSON clients. <img src> works, but an API consumer usually wants base64. Two options: point the client at /captcha with Cache-Control: no-store and read the src, or generate the builder yourself in a custom controller (new \Gregwar\Captcha\CaptchaBuilder($phrase), ->build(), ->get() for PNG bytes, ->inline()/->output() for a data URI). For key-based stateless APIs, store the phrase in Redis under a UUID you return to the client alongside the image, and validate the UUID + code pair — this avoids depending on cookies entirely.
Customize through config, not by patching. Width, height, length, and charset are all config values. If you need a different route, different fonts, or a JSON response, write your own controller and leave the plugin's route unregistered; the underlying CaptchaBuilder exposes font, distortion, and background options you can pass through.
Layer it with rate limiting. A captcha is a speed bump, not bot protection. Pair it with webman/rate-limiter on the login endpoint, and consider a slider/interactive challenge for high-value flows — the plugin only produces a fixed-phrase image.
extension=gd (with FreeType for TTF fonts) is enabled in the PHP CLI that runs webman. A missing GD surfaces as a blank image or a worker exception, not a friendly error.GET /captcha. Without Cache-Control: no-store, Chrome and proxies will happily reuse the previous image. Always append a cache-busting query (?t=...) or set the header explicitly.Session::regenerate() — regenerating first can drop the stored phrase and produce a "wrong captcha" error on a correct code./captcha route. If you already have a controller on that path, one silently shadows the other. Fix the ordering in config/route.php or change the plugin's route.Captcha::check in your test harness) rather than disabling captchas in production.gregwar/captcha, which sees infrequent releases, so lock versions in composer.json and be prepared to fork (MIT) if PHP-version deprecations appear. Keep your own controller thin so a future swap to a different captcha library is a one-file change.^1.0.5 or newer rather than leaving the constraint open down to older tags; if you are on 8.1–8.3, the upgrade is still worth taking but changes nothing for you. Test on 8.4 in CI before rolling it to workers that generate images, since GD/FreeType behavior is the piece most likely to differ across PHP minors.How can I help you explore Laravel packages today?