occ-therapist / form-request-validation-for-filament
Use Laravel Form Request validation rules inside Filament schemas.
Package info
github.com/OccTherapist/form-request-validation-for-filament
pkg:composer/occ-therapist/form-request-validation-for-filament
Requires
- php: ^8.2
- filament/filament: ^4.0|^5.0
- illuminate/contracts: ^11.0|^12.0|^13.0
- illuminate/http: ^11.0|^12.0|^13.0
- illuminate/support: ^11.0|^12.0|^13.0
- illuminate/validation: ^11.0|^12.0|^13.0
- spatie/laravel-package-tools: ^1.16
Requires (Dev)
- mockery/mockery: ^1.6
- orchestra/testbench: ^9.0|^10.0
- pestphp/pest: ^3.0|^4.0
- pestphp/pest-plugin-laravel: ^3.0|^4.0
README
Use Laravel Form Request validation inside Filament schemas. Define your rules, messages, and attributes once — reuse them across API routes, controllers, and Filament forms.
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 and its validation rules are automatically applied to the matching fields.
Features
- Form Request integration — uses
rules(),messages(), andattributes()from your existing Form Requests - Automatic field mapping — matches rules to fields by name (
email→TextInput::make('email')) - Wildcard support — maps nested rules like
items.*.nameto repeater children - Context-aware — choose different Form Requests per page (create vs. edit) via callback
- Dynamic rules — rules are re-resolved on every validation, so
required_ifand similar rules work with live form state - Request simulation — provides route parameters and input context to Form Requests (e.g.
Rule::unique()->ignore($this->route('user'))) - Smart rule merging — combines existing field rules with Form Request rules; Form Request wins on conflicts
- Orphan rule handling — rules without a matching field still run on submit; failures show a Filament notification
- Validated data helper — optional
getFormRequestValidated()after successful validation - Filament 4 & 5 — single package with an internal adapter layer
Requirements
| Dependency | Version |
|---|---|
| PHP | 8.2+ |
| Laravel | 11, 12, or 13 |
| Filament | 4.x or 5.x |
Installation
composer require occ-therapist/form-request-validation-for-filament
The package auto-registers via Laravel's package discovery. No manual service provider 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')->email(), ]) ->formRequest( class: fn () => $this instanceof EditRecord ? UpdateUserRequest::class : StoreUserRequest::class, ); }
Important: Call
->formRequest()after->components()so the internal validation hook is appended correctly.
Validation errors appear directly on the matching input fields, just like native Filament validation.
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 for the current context. Receives $livewire via dependency injection. |
mergeInput |
Closure |
No | Merges additional data into the simulated request input before rules are resolved. Receives $state and $livewire. |
class callback
Use this to select the right Form Request depending on the page or context:
->formRequest( class: fn () => match (true) { $this instanceof CreateRecord => StorePostRequest::class, $this instanceof EditRecord => UpdatePostRequest::class, default => StorePostRequest::class, }, )
mergeInput callback
Some Form Requests rely on data that is not part of the visible form state. Use mergeInput to enrich the simulated request:
->formRequest( class: fn () => UpdateUserRequest::class, mergeInput: fn (array $state) => [ ...$state, 'role' => $this->record?->role, 'tenant_id' => filament()->getTenant()?->id, ], )
This is useful for:
- Fields stored on the record but not shown in the form
- Route-like parameters needed by
Rule::unique()->ignore($this->route('user')) - Conditional rules that depend on values outside the form
Field mapping
Rules are matched to schema fields automatically by name. No extra configuration per field is needed.
| Form Request rule key | Filament field | Match |
|---|---|---|
email |
TextInput::make('email') |
Exact |
address.street |
TextInput::make('address.street') |
Dot notation |
items.*.name |
Repeater child TextInput::make('name') |
Wildcard |
name |
TextInput::make('data.name') |
By field name |
Repeaters & nested fields
Form Requests commonly validate repeater data with wildcard keys:
// In your Form Request 'items.*.name' => ['required', 'string', 'max:255'], 'items.*.quantity' => ['required', 'integer', 'min:1'],
// In your Filament schema Repeater::make('items') ->schema([ TextInput::make('name'), TextInput::make('quantity')->numeric(), ])
The plugin maps items.*.name to each repeater item's name field automatically.
Rule merging
If 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 when rules conflict (e.g.
nullablevs.required) - Identical rules are deduplicated
- Non-conflicting field rules are kept (e.g.
stringfrom->email())
Orphan rules
Rules in the Form Request that have no matching schema field are called orphan rules. They still run during validation but cannot be displayed on an input.
// Form Request 'terms_accepted' => ['accepted'],
If there is no Checkbox::make('terms_accepted') in the schema, a failed validation shows a Filament notification instead of an inline field error.
Tip: Add a matching field (visible or hidden) to show the error inline:
Checkbox::make('terms_accepted')->label('I accept the terms'), // or Hidden::make('terms_accepted'),
Accessing validated data
After successful validation, use the InteractsWithFormRequestValidation trait to retrieve the validated payload:
use OccTherapist\FormRequestValidationForFilament\Concerns\InteractsWithFormRequestValidation; class CreateUser extends CreateRecord { use InteractsWithFormRequestValidation; protected function handleRecordCreation(array $data): Model { $data = $this->getFormRequestValidated(); return User::create($data); } }
getFormRequestValidated() runs Laravel's validator with the Form Request's rules against the resolved input — the same data that was used to evaluate dynamic rules.
How it works
Schema::formRequest()
│
▼
Form Request resolved with simulated HTTP request
(form state + optional mergeInput + route parameters)
│
▼
rules(), messages(), attributes() extracted
│
▼
Rules mapped to schema fields (exact + wildcard)
│
▼
Merged with existing field rules → applied on validation
│
▼
Orphan rule failures → Filament notification
- A hidden validation hook is injected into the schema when
formRequest()is called. - On each validation, the Form Request is instantiated with a simulated request containing the current form state.
- Rules are mapped to fields by name and applied through Filament's native validation pipeline.
- Errors appear on the matching input fields; orphan errors trigger a notification.
Action modals
Attach a Form Request directly to an action form schema:
use Filament\Actions\Action; 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 { // ... });
Action forms use Livewire state paths like mountedActions.0.data.email internally — the plugin normalizes these automatically.
Wizards
Define one Form Request for the entire wizard and attach it to the root schema:
Wizard::make([ Step::make('Account')->schema([ TextInput::make('email'), ]), Step::make('Profile')->schema([ TextInput::make('name'), ]), ])
return $schema ->components([/* wizard */]) ->formRequest(class: fn () => StoreUserRequest::class);
On each step, only the fields in the active step are validated. Rules for later steps are ignored until the full form is submitted.
If your Form Request needs data from other steps during validation, use mergeInput:
->formRequest( class: fn () => StoreUserRequest::class, mergeInput: fn (array $state, Component $livewire): array => [ ...($livewire->data ?? []), ...$state, ], )
Relation managers
Relation manager create/edit actions use the same schema API as resource pages:
class PostsRelationManager extends RelationManager { public function form(Schema $schema): Schema { return $schema ->components([ TextInput::make('title'), ]) ->formRequest( class: fn () => $this->getOwnerRecord()->exists ? UpdatePostRequest::class : StorePostRequest::class, ); } }
For modal actions on the relation manager table, attach formRequest() inside the action schema callback.
Table filters
Attach a Form Request to your table filters using filtersFormRequest() on the table configuration:
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'], ]; }
Filter fields use state paths like tableDeferredFilters.{filterName}.{field} — the plugin normalizes these automatically.
- Deferred filters (default): validation runs when the user clicks Apply
- Live filters (
->deferFilters(false)): validation runs when filter values change
What is not supported (yet)
The following Form Request features are not part of v1:
| Feature | Status |
|---|---|
rules() |
Supported |
messages() |
Supported |
attributes() |
Supported |
authorize() |
Not supported — use Filament policies instead |
prepareForValidation() |
Not supported |
withValidator() |
Not supported |
passedValidation() / failedValidation() |
Not supported |
Supported contexts
| Context | Version |
|---|---|
| Resource Create / Edit pages | v1.0 |
| Standalone forms / Settings pages | v1.0 |
| Action modals | v1.1 |
| Wizards (per-step validation) | v1.1 |
| Relation managers | v1.1 |
| Table filters | v1.2 |
Testing
composer test
Contributing
Contributions are welcome! Please open an issue or pull request on GitHub.
License
MIT © occTherapist