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

Laminas Captcha Laravel Package

laminas/laminas-captcha

Laminas Captcha generates and validates CAPTCHAs for PHP applications, with adapters for image, Figlet, and ReCaptcha challenges. It integrates with laminas-validator and forms to help protect sites from automated submissions.

Deep Wiki
Context7

Getting Started

laminas/laminas-captcha is a framework-agnostic adapter layer over three CAPTCHA implementations: Figlet (ASCII art), Image (GD-rendered PNG), and ReCaptcha (Google v2, delegated to laminas/laminas-recaptcha). There is no Laravel service provider and no auto-discovery — you wire everything yourself.

composer require laminas/laminas-captcha laminas/laminas-text
# only if you want the ReCaptcha adapter:
composer require laminas/laminas-recaptcha

laminas/laminas-session and laminas/laminas-stdlib arrive as hard dependencies; laminas/laminas-text is needed for Figlet/Image, and GD must be enabled for the Image adapter.

Where to look first: ignore the repo README diagram and read src/AbstractAdapter.php and src/AbstractWord.php. Everything you need to know (session persistence, generate(), render(), isValid()) lives in those two classes; the three adapters are thin subclasses. The Laminas\Captcha\Factory class is the only other file worth skimming.

First use case — replace a hand-rolled honeypot on a contact form with Figlet, purely to learn the lifecycle:

use Laminas\Captcha\Figlet;

$captcha = new Figlet([
    'name' => 'captcha',                                  // required, see Gotchas
    'font' => resource_path('fonts/figlet/larry3d.flf'),  // absolute path
]);
$captcha->generate();            // picks a random word + id, persists to session
$html = $captcha->render();      // "<pre>...</pre>" + hidden id input

Then validate on submit:

if (! $captcha->isValid($request->input('captcha'))) {
    // $request->input('captcha') === ['id' => '...', 'input' => '...']
    return back()->withErrors(['captcha' => $captcha->getMessages()]);
}

Do not skip straight to Image — the session plumbing is identical, but the Image adapter adds filesystem and public-URL concerns that will mask the real problem if the session bridge is wrong.

Implementation Patterns

Config-driven adapter factory

Don't instantiate adapters inline in controllers. Register one small factory that maps a config key to a configured adapter:

// config/captcha.php
return [
    'default' => env('CAPTCHA_DRIVER', 'image'),
    'drivers' => [
        'image' => [
            'class'     => \Laminas\Captcha\Image::class,
            'name'      => 'captcha',
            'imgDir'    => storage_path('app/public/captcha'),
            'imgUrl'    => '/storage/captcha',
            'font'      => resource_path('fonts/DejaVuSans.ttf'),
            'width'     => 220,
            'height'    => 80,
            'wordLen'   => 6,
            'fontSize'  => 28,
            'expiration'=> 300,
        ],
        'figlet' => [ /* ... */ ],
        'recaptcha' => [
            'class'   => \Laminas\Captcha\ReCaptcha::class,
            'name'    => 'g-recaptcha-response',
            'pubkey'  => env('RECAPTCHA_SITE_KEY'),
            'privkey' => env('RECAPTCHA_SECRET'),
        ],
    ],
];
// app/Services/CaptchaManager.php
public function make(?string $driver = null): AbstractAdapter
{
    $cfg = config('captcha.drivers.' . ($driver ?? config('captcha.default')));
    return \Laminas\Captcha\Factory::factory($cfg);   // handles 'class' key
}

Factory::factory() will pick the adapter from the class key (or the short name image/figlet/recaptcha) and pass the rest as options.

Generate in the GET handler, validate in the POST handler

The adapter is cheap to construct and its only state is (a) the id/word pair and (b) the session entry. So the idiomatic Laravel split is:

// GET /contact
public function create(CaptchaManager $captchas)
{
    $captcha = $captchas->make();
    $captcha->generate();
    return view('contact', ['captcha' => $captcha->render()]);
}
// POST /contact
public function store(ContactRequest $request, CaptchaManager $captchas)
{
    // A *fresh* instance with the same session container; the word is
    // rehydrated from the session by id.
    $captcha = $captchas->make();
    $captcha->isValid($request->input('captcha'));   // sets its own messages
}

Never call generate() on POST — it overwrites the session word and every submission fails with "badCaptcha".

A custom validation rule

Wrap isValid() in a ValidationRule so it composes with FormRequests:

class ValidCaptcha implements ValidationRule
{
    public function __construct(private string $driver) {}

    public function validate(string $attribute, mixed $value, Closure $fail): void
    {
        $captcha = app(CaptchaManager::class)->make($this->driver);
        $payload = is_array($value) ? $value : ['id' => request('captcha.id'), 'input' => $value];

        if (! $captcha->isValid($payload)) {
            $fail(collect($captcha->getMessages())->first() ?: 'Invalid CAPTCHA.');
        }
    }
}
$request->validate([
    'captcha.input' => ['required', new ValidCaptcha('image')],
]);

Rendering in Blade

render() returns HTML you did not author, so emit it raw and supply the text input yourself:

{!! $captcha !!}
<input type="text" name="captcha[input]" autocomplete="off" required>

If you want to control markup entirely, skip render() and build the <img>/hidden input by hand using $captcha->getId() and $captcha->getImgUrl() . $captcha->getId() . '.png'.

Deterministic tests

setWord() before generate() is respected, which is the only sane way to test the flow:

$captcha = new Figlet(['font' => ..., 'name' => 'captcha']);
$captcha->setWord('ABC123')->generate();
$this->post('/contact', ['captcha' => ['id' => $captcha->getId(), 'input' => 'ABC123']])
     ->assertSessionHasNoErrors();

For ReCaptcha, use Google's published test keys (always-pass / always-fail) and fake the HTTP client rather than hitting Google in CI.

Gotchas and Tips

  • Set name explicitly. Standalone render() uses $this->getName() to build the hidden input. Leave it null and you get name="[id]" plus an unusable POST shape. This is the single most common "it renders but never validates" bug.
  • Session is the whole ballgame. AbstractWord persists the word in a Laminas\Session\Container (default namespace Laminas\Captcha\...) backed by $_SESSION. Laravel's session middleware does not call PHP's native session_start(), so in a Laravel app you must either (a) start a Laminas SessionManager with a distinct cookie name during boot, or (b) implement Laminas\Session\Storage\StorageInterface delegating to Laravel's Store and inject it via setSession(new Container('captcha', $manager)). Option (a) is faster to ship; option (b) is what you want if you care about cookie collisions or running behind an unusual session driver.
  • isValid() takes an array, not a string. For Figlet/Image the contract is ['id' => ..., 'input' => ...]. Passing the raw string produces missingValue, not badCaptcha, which sends you chasing the wrong bug.
  • Image directory must be writable and publicly served. imgDir is a filesystem path, imgUrl a URL path — they are unrelated options and a mismatch yields broken images with no error. On Laravel Cloud/Forge ephemeral filesystems, put imgDir on a persistent disk. Old PNGs are only cleaned by a probabilistic gcFreq sweep during generate(); add a scheduled captcha:prune job for anything real.
  • Font paths are absolute and mandatory. No default font is bundled. A relative path throws NoFontProvidedException at generate()/render() time, not at construction.
  • expiration defaults bite. The word expires on a timer (default 300s). Users who leave a tab open get a generic failure. Surface a distinct "refresh the page" message rather than "invalid code".
  • ReCaptcha reads $_SERVER['REMOTE_ADDR'] for remoteip verification. Behind a load balancer or Cloudflare this may be an internal IP and Google will reject. Set the remote IP on the underlying Laminas\ReCaptcha\ReCaptcha service before validating, or trust-proxy correctly.
  • ReCaptcha v2 only. There is no v3, hCaptcha, Turnstile, or Friendly Captcha adapter. If you need any of those (you probably do in 2026), this package will only carry you for the Figlet/Image cases — plan to write your own adapter implementing Laminas\Captcha\AdapterInterface.
  • Figlet and Image are cosmetic, not security. Both are trivially defeated by modern OCR and by simply reading the PNG. Treat them as spam friction, pair them with rate limiting on the form endpoint, and never use them on auth-critical flows.
  • getMessages() is not Laravel-shaped. It returns a [key => message] map (missingValue, badCaptcha, errNotSame). Override per-adapter with the message option or setMessage(), and translate inside your rule — Laravel's lang/*/validation.php won't see them.
  • Extension point: AbstractAdapter::isValid() and render() are the only methods your rule/Blade layer touches. Subclassing is the cleanest escape hatch — e.g. a LaravelSessionAdapter that overrides getSession() to return a Laravel-backed container, or an HCaptcha adapter overriding isValid()/render(). The base classes are small and stable enough that this is low-risk.
  • Vendor risk is real. 25 stars, low ecosystem adoption, and a Laminas-only release cadence. Pin the major version, keep the AdapterInterface boundary in your own CaptchaManager, and don't let adapter-specific types leak into controllers — swapping to a Laravel-native solution later should be a one-file change.
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.
socialdept/atp-orm
cboxdk/laravel-assistant-client
socialdept/atp-resolver
socialdept/tenant-domains
tautvydasr/blade-class-names
socialdept/color-scales
maherelgamil/controller
coleus/widgets
coleus/table
coleus/users
coleus/music
coleus/health
coleus/calendar
coleus/notes
coleus/support
coleus/settings
coleus/apps
seampact/laravel
emmyconceptng/laravel-ng-postcode
jjsquady/round-robin