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

Filament Captcha Field Laravel Package

yepsua/filament-captcha-field

yepsua/filament-captcha-field adds a ready-made CAPTCHA form field to Filament admin panels, helping you protect forms from bots and spam with minimal setup. Drop it into resources and custom forms to validate submissions and keep automated abuse out of your Laravel app.

View on GitHub
Deep Wiki
Context7

Getting Started

The package gives you a single Filament form component — a captcha image plus a text input — that validates server-side using the companion yepsua/captcha session store. If you're adding a public-facing form (contact, registration, password reset, a "request a quote" resource) and you want a captcha without hand-rolling an image route, this is the shortest path.

composer require yepsua/filament-captcha-field

Check constraints before you get attached. The most recent release, v0.1.1, only relaxes the Laravel constraint to allow Laravel 10 — it does not touch the Filament constraint, which is still ^2.0-era and predates Filament v3. So on a Laravel 10 + Filament v2 app the install is clean; on Laravel 10 + Filament v3/v4 you'll resolve the package fine and then hit API breakage at runtime. Don't read a successful composer require as a compatibility signal. If you're on Laravel 11+, expect to need --ignore-platform-reqs or a fork anyway.

Where to look first, in this order:

  1. vendor/yepsua/filament-captcha-field/README.md — the API surface is small; the README is the whole manual.
  2. vendor/yepsua/filament-captcha-field/src/Forms/Components/Captcha.php — the field class. Read setUp(), getView(), and anything that touches session().
  3. vendor/yepsua/captcha/ — the actual image generation and the validation rule. The rule name and session key live here, not in the Filament package.
  4. php artisan vendor:publish to see whether a config file is offered (filament-captcha-field / captcha tags).

First use case — a resource form:

use Yepsua\FilamentCaptchaField\Forms\Components\Captcha;

public static function form(Form $form): Form
{
    return $form->schema([
        TextInput::make('name')->required(),
        TextInput::make('email')->email()->required(),
        Textarea::make('message')->required(),

        Captcha::make('captcha')
            ->label('Verification')
            ->required()
            ->dehydrated(false),   // never write this to the model
    ]);
}

Render it, submit once with the correct characters, once with garbage, and confirm both branches. That three-minute test tells you whether sessions, routes, and the validation rule are all wired before you build anything on top of it.

Implementation Patterns

Keep it out of persistence. The captcha value is transient — always ->dehydrated(false). Anything else and you'll get Column not found: captcha or a junk column in your table. For CreateRecord pages that's the whole story; for EditRecord pages you generally want the field only on create:

Captcha::make('captcha')
    ->required()
    ->dehydrated(false)
    ->visible(fn (string $operation) => $operation === 'create'),

Pair it with rate limiting, not instead of it. A captcha stops naive bots; it doesn't stop a determined one. Wrap the page's action with a throttle or add spatie/laravel-honeypot alongside — the two are complementary and cost almost nothing.

Login / registration pages. Filament's auth pages expose form() hooks via a panel plugin or by subclassing the page. This is the highest-value placement (credential stuffing) and also the most fragile, because the auth pages are Livewire components with their own lifecycle:

// in a custom Login page extending Filament\Pages\Auth\Login
protected function getForms(): array
{
    return [
        'form' => $this->form(
            $this->makeForm()
                ->schema([
                    $this->getEmailFormComponent(),
                    $this->getPasswordFormComponent(),
                    Captcha::make('captcha')->required()->dehydrated(false),
                    $this->getRememberFormComponent(),
                ])
                ->statePath('data'),
        ),
    ];
}

Test the failed-login path specifically: an incorrect captcha should short-circuit before the credential check, and the error should land on the captcha field, not on email.

Reuse via a schema macro or a trait. If you put captchas on five forms, centralise the configuration so a width/length change is one edit:

// AppServiceProvider::boot()
Form::macro('captcha', fn () => Captcha::make('captcha')
    ->required()
    ->dehydrated(false)
    ->helperText('Type the characters shown above.'));

Disable it in non-production and tests. Every captcha package needs an escape hatch, otherwise your feature suite becomes a coin flip. Gate it on config and set that config to false in phpunit.xml and .env.testing:

->visible(fn () => config('app.captcha_enabled', true))
->required(fn () => config('app.captcha_enabled', true))

Then a Livewire test is just a normal form submission:

Livewire::test(CreateContact::class)
    ->fillForm(['name' => 'Ada', 'email' => '[email protected]', 'message' => 'Hi'])
    ->call('create')
    ->assertHasNoFormErrors();

If you can't disable it, mock the underlying validation rule or Session::shouldReceive(...) — but the config flag is far less brittle.

Laravel 10 users get the same deal as everyone else. v0.1.1 makes the install possible on L10; it does not make the field v3-compatible or add any behaviour. Everything above still applies unchanged.

Refresh UX. The image is generated per-request with a token; a "click to reload" affordance in the field view is worth verifying works inside Livewire's DOM diffing. If the package's refresh triggers a full wire:model update, users typing fast can lose their input to a re-render — see gotchas below.

Gotchas and Tips

Filament version drift is the headline risk — and v0.1.1 does not change it. The only release since February 2023 is a one-line Laravel constraint bump. There is still no Filament v3/v4 support. In Filament v3 the Field base class changed (view resolution, afterStateHydrated, dehydrated, the ->rules() plumbing). Before you invest, run composer why-not filament/filament and read the field class to see how much of it is Filament v2 API. Porting it is usually 30–60 lines — the class is thin — but you should budget for it rather than be surprised.

Don't mistake a dependency bump for maintenance. A release that only widens illuminate/* constraints is the cheapest possible commit. It tells you the package installs, not that it's supported. Same conclusion as before: if it's load-bearing on a public form, fork it into your own namespace and own it.

Session coupling. The captcha is stored in the session and consumed on validation. That means:

  • Any middleware that regenerates the session ID between render and submit (or a config where SESSION_DRIVER=array) will produce a permanent "invalid captcha."
  • Two forms open in two tabs will fight over the session entry. If you need captchas on multiple forms simultaneously, verify each field carries its own token key rather than a single global one.
  • Panels using a different session domain or ->domain() will not share the session with your image route. Symptom: the image renders (it's a public GET) but validation always fails.

The image route and APP_URL. The <img src> is built from your app URL. Behind a proxy, on a subdirectory install, or with ASSET_URL set, a mismatched URL yields a broken image and a guaranteed validation failure. php artisan route:list | grep -i captcha and open the URL manually — five seconds, saves an hour.

Config caching. If you publish a config file for dimensions/length/colors, php artisan config:cache in your deploy script, and clear it after the change. A stale cached config is a classic "works locally, broken in staging" for image-generating packages.

Dehydration and validation order. ->dehydrated(false) removes the value from the form state but does not stop validation — confirm the field's rules() still fire. If you also add ->rules(['captcha']), check whether the field already appends that rule internally; a duplicate rule is harmless, a missing one is a silent no-op captcha.

Livewire re-renders. Typing in other fields on the form triggers a round-trip. If the captcha component regenerates on every render, the user's answer is invalidated mid-typing. Test by filling the captcha first, then a text field, then submitting. If it breaks, cache the token in the component's mount() rather than the render cycle — that's the fix you'd apply in a fork.

Accessibility and legal exposure. A character-image captcha with no audio alternative fails WCAG 1.1.1 and (in the EU) accessibility requirements for public-facing services. For anything customer-facing, plan a fallback: an email-confirmation flow, a honeypot field, or a "contact us directly" link. Don't ship pure image captcha on a registration form without one.

Extension points that actually exist. The field view is overridable via ->view('forms.components.my-captcha') if you want to restyle it. The image generator's config (length, width, height, fonts, noise) is in the companion package's config. The validation rule name is in the companion package — if you need custom error messages, ->validationMessages(['captcha' => '...']) is the clean hook rather than editing vendor code.

Debugging checklist when "invalid captcha" is unconditional:

  1. route:list — is the image route registered and does it return 200?
  2. session()->getId() before render vs. at submit — is it stable?
  3. config('session.driver') — not array, not cookie with a bad domain.
  4. php artisan config:clear — stale cache after publishing config.
  5. Log the expected vs. submitted value in the validation rule temporarily — the fastest way to distinguish "wrong session" from "wrong answer."
  6. Check your resolved Filament major — composer show filament/filament — before blaming sessions for what is actually a v2 API mismatch.

Maintenance posture. At ~18 stars, 0 dependents, and a single constraint-bump release after a two-year gap, treat this as vendored code you happen to install. If it's load-bearing on a public form, fork it into your own namespace, add tests for the session/validation round-trip, and port it to your Filament major. For a one-off internal admin form on Filament v2 (now including Laravel 10 apps), using it as-is is fine — just accept it will not be updated for you.

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.
diephp/sequences
users-au/socialite-provider
twidpay/twid-logger
mrnewport/laravel-plaid
mrnewport/laravel-repo
mrnewport/laravel-docsign
mrnewport/laravel-stow
mrnewport/laravel-flow
mrnewport/laravel-priceable
mrnewport/laravel-nestedterms
fredmeng/laravel-apidoc-generator
itstructure/laravel-multilingual-tools
pdustdar/elasticsearch
markupai/php-sdk
rlacerda83/lumem-microservice-shipment
uni-method/symfony-lib
tacticmedia/scim-server-bundle
neika-ai/symfony-bundle
mattleads/agent-publishing-bundle
glavweb/framework-bundle