## 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