## Filament
- Filament is a Laravel UI framework built on Livewire, Alpine.js, and Tailwind CSS. UIs are defined in PHP via fluent, chainable components. Follow existing conventions in this app.
- Use the `search-docs` tool for official documentation on Artisan commands, code examples, testing, relationships, and idiomatic practices. If `search-docs` is unavailable, refer to https://filamentphp.com/docs.
### Artisan
- Always use Filament-specific Artisan commands to create files. Find available commands with the `list-artisan-commands` tool, or run `php artisan --help`.
- Inspect required options before running, and always pass `--no-interaction`.
### Patterns
Always use static `make()` methods to initialize components. Most configuration methods accept a `Closure` for dynamic values.
Use `Get $get` to read other form field values for conditional logic:
@verbatim
use Filament\Forms\Components\Select;
use Filament\Forms\Components\TextInput;
use Filament\Schemas\Components\Utilities\Get;
Select::make('type')
->options(CompanyType::class)
->required()
->live(),
TextInput::make('company_name')
->required()
->visible(fn (Get $get): bool => $get('type') === 'business'),
@endverbatim
Use `Set $set` inside `->afterStateUpdated()` on a `->live()` field to mutate another field reactively. Prefer `->live(onBlur: true)` on text inputs to avoid per-keystroke updates:
@verbatim
use Filament\Schemas\Components\Utilities\Set;
use Illuminate\Support\Str;
TextInput::make('title')
->required()
->live(onBlur: true)
->afterStateUpdated(fn (Set $set, ?string $state) => $set(
'slug',
Str::slug($state ?? ''),
)),
TextInput::make('slug')
->required(),
@endverbatim
Compose layout by nesting `Section` and `Grid`. Children need explicit `->columnSpan()` or `->columnSpanFull()`:
@verbatim
use Filament\Schemas\Components\Grid;
use Filament\Schemas\Components\Section;
Section::make('Details')
->schema([
Grid::make(2)->schema([
TextInput::make('first_name')
->columnSpan(1),
TextInput::make('last_name')
->columnSpan(1),
TextInput::make('bio')
->columnSpanFull(),
]),
]),
@endverbatim
Use `Repeater` for inline `HasMany` management. `->relationship()` with no args binds to the relationship matching the field name:
@verbatim
use Filament\Forms\Components\Repeater;
Repeater::make('qualifications')
->relationship()
->schema([
TextInput::make('institution')
->required(),
TextInput::make('qualification')
->required(),
])
->columns(2),
@endverbatim
Use `state()` with a `Closure` to compute derived column values:
@verbatim
use Filament\Tables\Columns\TextColumn;
TextColumn::make('full_name')
->state(fn (User $record): string => "{$record->first_name} {$record->last_name}"),
@endverbatim
Use `SelectFilter` for enum or relationship filters, and `Filter` with a `->query()` closure for custom logic:
@verbatim
use Filament\Tables\Filters\Filter;
use Filament\Tables\Filters\SelectFilter;
use Illuminate\Database\Eloquent\Builder;
SelectFilter::make('status')
->options(UserStatus::class),
SelectFilter::make('author')
->relationship('author', 'name'),
Filter::make('verified')
->query(fn (Builder $query) => $query->whereNotNull('email_verified_at')),
@endverbatim
Actions are buttons that encapsulate optional modal forms and behavior:
@verbatim
use Filament\Actions\Action;
Action::make('updateEmail')
->schema([
TextInput::make('email')
->email()
->required(),
])
->action(fn (array $data, User $record) => $record->update($data)),
@endverbatim
### Testing
Testing setup (requires `pestphp/pest-plugin-livewire` in `composer.json`):
- Always call `$this->actingAs(User::factory()->create())` before testing panel functionality.
- For edit pages, pass `['record' => $user->id]`, use `->call('save')` (not `->call('create')`), and do not assert `->assertRedirect()` (edit pages do not redirect after save).
@verbatim
use function Pest\Livewire\livewire;
livewire(ListUsers::class)
->assertCanSeeTableRecords($users)
->searchTable($users->first()->name)
->assertCanSeeTableRecords($users->take(1))
->assertCanNotSeeTableRecords($users->skip(1));
use function Pest\Laravel\assertDatabaseHas;
livewire(CreateUser::class)
->fillForm([
'name' => 'Test',
'email' => 'test@example.com',
])
->call('create')
->assertNotified()
->assertHasNoFormErrors()
->assertRedirect();
assertDatabaseHas(User::class, [
'name' => 'Test',
'email' => 'test@example.com',
]);
livewire(EditUser::class, ['record' => $user->id])
->fillForm(['name' => 'Updated'])
->call('save')
->assertNotified()
->assertHasNoFormErrors();
assertDatabaseHas(User::class, [
'id' => $user->id,
'name' => 'Updated',
]);
livewire(CreateUser::class)
->fillForm([
'name' => null,
'email' => 'invalid-email',
])
->call('create')
->assertHasFormErrors([
'name' => 'required',
'email' => 'email',
])
->assertNotNotified();
@endverbatim
Use `->callAction(DeleteAction::class)` for page actions, or `->callAction(TestAction::make('name')->table($record))` for table actions:
@verbatim
use Filament\Actions\Testing\TestAction;
livewire(ListUsers::class)
->callAction(TestAction::make('promote')->table($user), [
'role' => 'admin',
])
->assertNotified();
@endverbatim
### Correct Namespaces
- Form fields (`TextInput`, `Select`, `Repeater`, etc.): `Filament\Forms\Components\`
- Infolist entries (`TextEntry`, `IconEntry`, etc.): `Filament\Infolists\Components\`
- Layout components (`Grid`, `Section`, `Fieldset`, `Tabs`, `Wizard`, etc.): `Filament\Schemas\Components\`
- Schema utilities (`Get`, `Set`, etc.): `Filament\Schemas\Components\Utilities\`
- Table columns (`TextColumn`, `IconColumn`, etc.): `Filament\Tables\Columns\`
- Table filters (`SelectFilter`, `Filter`, etc.): `Filament\Tables\Filters\`
- Actions (`DeleteAction`, `CreateAction`, etc.): `Filament\Actions\`. Never use `Filament\Tables\Actions\`, `Filament\Forms\Actions\`, or any other sub-namespace for actions.
- Icons: `Filament\Support\Icons\Heroicon` enum (e.g., `Heroicon::PencilSquare`)
### Common Mistakes
- **Never assume public file visibility.** File visibility is `private` by default. Always use `->visibility('public')` when public access is needed.
- **Never assume full-width layout.** `Grid`, `Section`, `Fieldset`, and `Repeater` do not span all columns by default.
- **Use `Select::make('author_id')->relationship('author', 'name')` for BelongsTo fields.** `BelongsToSelect` does not exist in v4.
- **`Repeater` uses `->schema()`, not `->fields()`.**
- **Never add `->dehydrated(false)` to fields that need to be saved.** It strips the value from form state before `->action()` or the save handler runs. Only use it for helper/UI-only fields.
- **Use correct property types when overriding `Page`, `Resource`, and `Widget` properties.** These properties have union types or changed modifiers that must be preserved:
- `$navigationIcon`: `protected static string | BackedEnum | null` (not `?string`)
- `$navigationGroup`: `protected static string | UnitEnum | null` (not `?string`)
- `$view`: `protected string` (not `protected static string`) on `Page` and `Widget` classes