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

Use Laravel Form Request validation rules inside Filament schemas.

Maintainers

Package info

github.com/OccTherapist/form-request-validation-for-filament

pkg:composer/occ-therapist/form-request-validation-for-filament

Transparency log

Statistics

Installs: 1 103

Dependents: 0

Suggesters: 0

Stars: 5

Open Issues: 0

v1.3.1 2026-08-25 14:01 UTC

This package is auto-updated.

Last update: 2026-08-25 14:09:10 UTC


README

Latest Version on Packagist Total Downloads Tests License GitHub stars

Form Request Validation for Filament

Use Laravel Form Request validation inside Filament — define your rules, messages, and attributes once, reuse them across API routes, controllers, and Filament UI.

Why this plugin?

Filament validates fields through dedicated methods like ->required() and ->email() on each input. That works well for simple forms, but validation logic often belongs in a central place — especially when the same rules already exist in a Form Request.

This plugin bridges that gap: attach a Form Request to your schema or table filters and its validation rules are automatically applied to the matching fields.

Features

  • Form Request integrationrules(), messages(), attributes(), and prepareForValidation()
  • Automatic field mapping — by field name, dot notation, and wildcards (items.*.name)
  • Array & pipe syntax'email' => ['required', 'email'] and 'email' => 'required|email'
  • Context-aware — different Form Requests per page via callback (create vs. edit)
  • Dynamic rules — re-resolved on every validation (required_if, Rule::unique(), etc.)
  • Request simulation — route parameters and input context for Form Requests
  • Smart rule merging — Form Request rules take precedence on conflicts
  • Orphan rule handling — unmatched rules show a Filament notification
  • getFormRequestValidated() — optional helper trait for validated data
  • Filament 4 & 5 — single package, internal adapter layer

Supported contexts

Context API
Resource Create / Edit Schema::formRequest()
Standalone forms / Settings pages Schema::formRequest()
Action modals Schema::formRequest()
Wizards (per-step validation) Schema::formRequest()
Relation managers Schema::formRequest()
Table filters Table::filtersFormRequest()

Requirements

Dependency Version
PHP 8.2+
Laravel 12 or 13
Filament 4.x or 5.x

Installation

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

Auto-registers via Laravel package discovery. No manual setup required.

Quick start

1. Create a Form Request

// app/Http/Requests/StoreUserRequest.php

namespace App\Http\Requests;

use Illuminate\Foundation\Http\FormRequest;

class StoreUserRequest extends FormRequest
{
    public function rules(): array
    {
        return [
            'name' => ['required', 'string', 'max:255'],
            'email' => ['required', 'email', 'unique:users'],
        ];
    }

    public function messages(): array
    {
        return [
            'email.unique' => 'This email address is already registered.',
        ];
    }

    public function attributes(): array
    {
        return [
            'name' => 'full name',
        ];
    }
}

2. Attach it to your Filament schema

use App\Http\Requests\StoreUserRequest;
use App\Http\Requests\UpdateUserRequest;
use Filament\Resources\Pages\CreateRecord;
use Filament\Resources\Pages\EditRecord;
use Filament\Schemas\Schema;

public function form(Schema $schema): Schema
{
    return $schema
        ->components([
            TextInput::make('name'),
            TextInput::make('email'),
        ])
        ->formRequest(
            class: fn () => $this instanceof EditRecord
                ? UpdateUserRequest::class
                : StoreUserRequest::class,
        );
}

Important: Call ->formRequest() after ->components() so the validation hook is appended correctly.

Validation errors appear directly on the matching input fields.

API reference

Schema::formRequest()

$schema->formRequest(
    class: fn (Component $livewire): string => StoreUserRequest::class,
    mergeInput: fn (array $state, Component $livewire): array => $state,
);
Parameter Type Required Description
class Closure Yes Returns the Form Request class. Receives $livewire via dependency injection.
mergeInput Closure No Merges additional data into the simulated request input. Receives $state and $livewire.

Context selection

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

Enriching input data

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

Useful when Form Requests depend on record data, route parameters, or values not visible in the form.

prepareForValidation()

Runs on the simulated Form Request before rules() are read. Use it to normalize or enrich input that rules() (or getFormRequestValidated()) depends on:

protected function prepareForValidation(): void
{
    $this->merge([
        'slug' => str($this->input('title'))->slug()->toString(),
        'type' => strtolower((string) $this->input('type')),
    ]);
}

Prepared input is stored for getFormRequestValidated(). Filament still validates the visible form state against the extracted rules — transforms here do not rewrite Livewire field values.

Table::filtersFormRequest()

$table->filtersFormRequest(
    class: fn () => FilterCustomersRequest::class,
    mergeInput: fn (array $state, Component $livewire): array => $state,
);

Same parameters as Schema::formRequest(). Attaches validation to the table's filter form.

Filter mode When validation runs
Deferred (default) When the user clicks Apply
Live (->deferFilters(false)) On every filter change

Usage by context

Resource pages

public function form(Schema $schema): Schema
{
    return $schema
        ->components([/* fields */])
        ->formRequest(class: fn () => StoreUserRequest::class);
}

Action modals

Action::make('invite')
    ->schema(fn (Schema $schema) => $schema
        ->components([
            TextInput::make('email'),
            TextInput::make('role'),
        ])
        ->formRequest(class: fn () => InviteUserRequest::class))
    ->action(function (array $data): void {
        // ...
    });

Wizards

One Form Request for the entire wizard — attach it to the root schema:

return $schema
    ->components([
        Wizard::make([
            Step::make('Account')->schema([
                TextInput::make('email'),
            ]),
            Step::make('Profile')->schema([
                TextInput::make('name'),
            ]),
        ]),
    ])
    ->formRequest(class: fn () => StoreUserRequest::class);

Only fields in the active step are validated per step. Use mergeInput if rules depend on data from other steps:

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

Relation managers

class PostsRelationManager extends RelationManager
{
    public function form(Schema $schema): Schema
    {
        return $schema
            ->components([
                TextInput::make('title'),
            ])
            ->formRequest(class: fn () => StorePostRequest::class);
    }
}

For modal actions on the table, attach formRequest() inside the action's schema callback.

Table filters

use Filament\Tables\Filters\Filter;
use Filament\Tables\Table;

public function table(Table $table): Table
{
    return $table
        ->filters([
            Filter::make('created')
                ->schema([
                    DatePicker::make('from'),
                    DatePicker::make('until'),
                ]),
        ])
        ->filtersFormRequest(class: fn () => FilterCustomersRequest::class);
}
// app/Http/Requests/FilterCustomersRequest.php
public function rules(): array
{
    return [
        'created.from' => ['nullable', 'date'],
        'created.until' => ['nullable', 'date', 'after_or_equal:created.from'],
    ];
}

Field mapping

Rules are matched to fields automatically by name — no per-field configuration needed.

Form Request key Filament field Match type
email TextInput::make('email') Exact
address.street TextInput::make('address.street') Dot notation
items.*.name Repeater child TextInput::make('name') Wildcard
created.from Filter field DatePicker::make('from') in Filter::make('created') Dot notation

State path normalization

Filament uses prefixed state paths internally. The plugin strips these automatically:

Internal state path Resolved key
data.email email
mountedActions.0.data.email email
tableDeferredFilters.created.from created.from

Rule merging

When a field already has Filament validation rules, both sets are merged:

TextInput::make('email')->email()->nullable()
// Form Request
'email' => ['required', 'email', 'unique:users']

Result: string, required, email, unique:users

  • Form Request rules take precedence on conflicts (nullable vs. required)
  • Identical rules are deduplicated
  • Non-conflicting field rules are kept

Orphan rules

Rules without a matching schema field still run on submit. Failures show a Filament notification instead of an inline field error.

To show the error on a field, add a matching input (visible or hidden):

Checkbox::make('terms_accepted')->label('I accept the terms'),
// or
Hidden::make('terms_accepted'),

Accessing validated data

use OccTherapist\FormRequestValidationForFilament\Concerns\InteractsWithFormRequestValidation;

class CreateUser extends CreateRecord
{
    use InteractsWithFormRequestValidation;

    protected function handleRecordCreation(array $data): Model
    {
        $data = $this->getFormRequestValidated();

        return User::create($data);
    }
}

How it works

formRequest() / filtersFormRequest()
        │
        ▼
  Form Request resolved with simulated HTTP request
  (form state + optional mergeInput + route parameters)
        │
        ▼
  prepareForValidation() runs (merge / normalize input)
        │
        ▼
  rules(), messages(), attributes() extracted
        │
        ▼
  Rules mapped to fields (exact + wildcard + state path normalization)
        │
        ▼
  Merged with existing field rules → applied on validation
        │
        ▼
  Orphan rule failures → Filament notification

Limitations

Form Request feature Status
rules() Supported
messages() Supported
attributes() Supported
prepareForValidation() Supported — runs before rules(); prepared input used by getFormRequestValidated() (does not rewrite Livewire field values)
authorize() Not supported — use Filament policies
withValidator() Not supported — Filament owns the validator lifecycle
passedValidation() / failedValidation() Not supported

Testing

composer test

Contributing

Contributions are welcome! Please open an issue or pull request on GitHub.

License

MIT © occTherapist