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

Form Request Validation For Filament Laravel Package

occ-therapist/form-request-validation-for-filament

Reuse Laravel Form Requests inside Filament: attach one to a schema or table filter and its rules, messages, and attributes apply automatically to matching fields, with wildcard mapping, context-aware per-page rules, and smart rule merging.

View on GitHub
Deep Wiki
Context7

Getting Started

Install (one command, no config to publish):

composer require occ-therapist/form-request-validation-for-filament

Auto-discovered. There is no config file, no migration, no asset build — the entire surface area is two fluent methods (Schema::formRequest() and Table::filtersFormRequest()).

Where to look first: your existing app/Http/Requests directory. The value proposition is only real if you already have Form Requests driving API/web controllers. If validation currently lives inline in Filament resources and nowhere else, this package adds a layer without removing one — start by consolidating rules into a Form Request first, then attach it.

Minimal working setup (3 steps):

  1. Create or reuse a Form Request with rules(), messages(), attributes().
  2. Add the matching Filament fields (names must line up with the rule keys).
  3. Call ->formRequest() after ->components():
public function form(Schema $schema): Schema
{
    return $schema
        ->components([
            TextInput::make('name'),
            TextInput::make('email'),
        ])
        ->formRequest(class: fn () => StoreUserRequest::class);
}

First use case worth shipping: a Create/Edit resource where the same StoreUserRequest/UpdateUserRequest already validates the JSON API. Attach both, branch on the page class, and you've removed the most common source of drift in Filament apps — the API saying max:255 while the form says max:100.

Verify before you build on it: write one Livewire test asserting a Form Request rule fires inline (assertHasFormErrors(['email' => 'unique'])). This is a 5-star, single-maintainer, zero-dependent package; the smoke test is cheaper than discovering a mismatch in production.

Implementation Patterns

Create vs. Edit via context callback. The class closure receives the Livewire component, so match (true) on the page class is the idiomatic form:

->formRequest(
    class: fn () => match (true) {
        $this instanceof CreateRecord => StorePostRequest::class,
        $this instanceof EditRecord   => UpdatePostRequest::class,
        default                       => StorePostRequest::class,
    },
)

Feeding non-form context into rules. unique and required_if rules that depend on tenant, record, or route data are the main friction point. Use mergeInput:

->formRequest(
    class: fn () => UpdateUserRequest::class,
    mergeInput: fn (array $state) => [
        ...$state,
        'tenant_id' => filament()->getTenant()?->id,
        'role' => $this->record?->role,
    ],
)

Treat this as the extension point of the package — it is the only hook you get into the simulated request.

Let the Form Request own the logic, the field own the presentation. Merging is a union with Form Request rules winning conflicts, so keeping ->email() / ->maxLength() on the input is harmless and often useful (it documents intent at the field level and survives if the schema is reused without a Form Request). Identical rules are deduplicated, so there is no cost.

Wizards: attach once at the root, not per step. Per-step attachment double-registers. Attach to the parent schema and the registry inherits config into steps; only the active step's fields validate. When step 2 depends on step 1 values, bridge with mergeInput:

mergeInput: fn (array $state, $livewire) => [...($livewire->data ?? []), ...$state],

Orphan rules are a UX lever, not a bug. Rules with no matching field still run on full submit and surface as a toast rather than an inline error. For terms_accepted, that toast is worse than a field error — add a real Checkbox (or Hidden::make('terms_accepted') if it's purely server-side) so the message lands where the user is looking.

Same pattern in every other context. Action modals (->schema(fn (Schema $s) => $s->components([...])->formRequest(...))), relation manager forms, and table filters all use the identical call signature. Filters have their own method:

$table->filtersFormRequest(class: fn () => FilterCustomersRequest::class);
// rules: 'created.from' => ['nullable', 'date'], 'created.until' => [...]

Extract validated data, don't trust $data. If you use the shipped trait, getFormRequestValidated() returns only what the Form Request validated — handy when the form state contains UI-only keys you don't want mass-assigned:

protected function handleRecordCreation(array $data): Model
{
    return User::create($this->getFormRequestValidated());
}

Test at the Livewire layer, not the unit layer. fillForm()->call('create')->assertHasFormErrors([...]) is what actually exercises mapping, merging, and state-path normalization. Unit-testing the Form Request alone proves nothing about this package.

Gotchas and Tips

Ordering is load-bearing. ->formRequest() must come after ->components(). Put it before and the validation hook is appended to a schema that doesn't yet have its fields — failures are silent rather than loud, so this is the first thing to check when "nothing happens."

authorize() is ignored. A Form Request whose authorize() returns false will not block the Filament form. Authorization must still be enforced by policies / canCreate() / canEdit() on the resource. Don't let a Form Request give you a false sense of security.

Three unsupported hooks bite in real apps: withValidator(), passedValidation()/failedValidation(), and prepareForValidation(). The README's limitations table lists prepareForValidation() as unsupported, while the v1.3.0 release notes claim it was added — this documentation conflict is unresolved in the sources. Verify empirically before you refactor logic that depends on it, and assume withValidator() (the usual home for cross-field conditional rules) is unavailable: express those conditions as Rule::when() / required_if inside rules() instead.

Laravel 11 is gone — check the lockfile, not the README. The requirements table still says "11, 12, or 13," but v1.3.1 dropped 11 because Composer refuses to install it due to unfixed security advisories. If your app is on Laravel 11, composer require will fail and the README will have misled you.

State paths are normalized for you — don't "fix" them. data.email, mountedActions.0.data.email, and tableDeferredFilters.created.from all resolve to email and created.from respectively. If a rule isn't matching, the bug is almost always a name mismatch, not a path problem. Rule key must equal Field::make('...') verbatim, including dot notation and wildcards (items.*.name for repeater children).

Repeater wildcards only map to child field names. items.*.name matches a TextInput::make('name') inside the repeater — not TextInput::make('items.*.name'). Getting this backwards produces orphan-rule toasts instead of inline errors.

Deferred vs. live filters change when validation runs. Default (deferred) filters validate on Apply; ->deferFilters(false) validates on every change. Live filters plus an expensive after_or_equal against a large dataset is a latency trap.

Dynamic rules are re-resolved on every validation. That's a feature (Rule::unique()->ignore(...), required_if reading current state), but it also means the Form Request is instantiated on each validation pass — avoid side effects in its constructor.

Orphan-rule toasts are your primary debugging signal. If a submit fails with a notification instead of a highlighted field, your rule key has no matching input. That's the fastest way to audit a mapping problem.

Dependency risk is real: 5 stars, 0 dependents, solo maintainer, internal adapter layer for Filament 4 and 5. Pin the version in composer.json, keep the smoke tests described above, and isolate usage behind a small number of schemas so a breaking Filament upgrade doesn't strand you mid-release cycle.

Known-good extension points, in order of usefulness: mergeInput → the class closure (context branching) → the InteractsWithFormRequestValidation trait. There is nothing else; if a requirement can't be met with those three, it can't be met with this package.

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.
tequilarapido/trackit
gregsphotography/site-builder-filament
cabaoctavio/laravel-query-log
gregsphotography/site-builder-components
io-digital/clickatell
snowsoft/laravel-admin
oxygen/auth
relicsoftware/barakah-payment-gateway
tobya/work-with-composer
equipc/laravel-email-validator
equipc/laravel-email-database-log
adedsec/nova-translated-labels
wazza/laravel-db-encryption
hokny/laravel-generator
vonsogt/backup
jengo/queues
jeremykenedy/laravel-observability
coleus/frame
thtg88/journalism
filagora/filagora-ai-forums