FilamentPHP Forms Generation Skill
Overview
This skill generates FilamentPHP v4 form schemas with proper field configurations, validation rules, relationships, and layout components.
Documentation Reference
CRITICAL: Before generating forms, read:
-
/home/mwguerra/projects/mwguerra/claude-code-plugins/filament-specialist/skills/docs/references/forms/
-
/home/mwguerra/projects/mwguerra/claude-code-plugins/filament-specialist/skills/docs/references/schemas/
Workflow
Step 1: Analyze Requirements
Identify:
-
Field types needed
-
Validation rules
-
Relationships (belongsTo, hasMany, etc.)
-
Layout preferences (sections, tabs, columns)
-
Conditional visibility
-
Custom formatting
Step 2: Read Documentation
Navigate to forms documentation and extract:
-
Exact field class names
-
Available methods and options
-
Validation integration patterns
-
Relationship handling
Step 3: Generate Schema
Build the form schema with proper structure:
use Filament\Forms; use Filament\Forms\Form; use Filament\Forms\Components\Section; use Filament\Forms\Components\Fieldset; use Filament\Forms\Components\Grid; use Filament\Forms\Components\TextInput; use Filament\Forms\Components\Select;
public static function form(Form $form): Form { return $form ->schema([ // Fields organized in sections/fieldsets ]); }
Schema Organization Requirement
CRITICAL: All form schemas MUST be organized using layout components. Never place fields directly at the root level of a form schema.
Minimum Organization Rules
-
Always use Sections or Fieldsets - Every form must have at least one Section or Fieldset wrapping its fields
-
Group related fields - Fields that belong together logically should be in the same Section/Fieldset
-
Use descriptive labels - Sections and Fieldsets should have meaningful titles
-
Consider collapsibility - Make sections collapsible when forms are long
Recommended Hierarchy
Form Schema ├── Section: "Primary Information" │ ├── Fieldset: "Basic Details" (optional grouping) │ │ ├── TextInput: name │ │ └── TextInput: email │ └── Fieldset: "Contact" (optional grouping) │ ├── TextInput: phone │ └── TextInput: address ├── Section: "Settings" │ ├── Toggle: is_active │ └── Select: status └── Section: "Media" (collapsible) └── FileUpload: avatar
Bad Example (DO NOT DO THIS)
// ❌ WRONG: Fields at root level without organization public static function form(Form $form): Form { return $form ->schema([ TextInput::make('name'), TextInput::make('email'), TextInput::make('phone'), Toggle::make('is_active'), FileUpload::make('avatar'), ]); }
Good Example (ALWAYS DO THIS)
// ✅ CORRECT: Fields organized in sections public static function form(Form $form): Form { return $form ->schema([ Section::make('Personal Information') ->description('Basic user details') ->schema([ TextInput::make('name') ->required() ->maxLength(255), TextInput::make('email') ->email() ->required(), TextInput::make('phone') ->tel(), ]),
Section::make('Settings')
->schema([
Toggle::make('is_active')
->label('Active')
->default(true),
]),
Section::make('Profile Image')
->collapsible()
->schema([
FileUpload::make('avatar')
->image()
->disk('public')
->directory('avatars'),
]),
]);
}
When to Use Section vs Fieldset
Component Use Case
Section Major logical groupings, can have description, icon, collapsible
Fieldset Smaller sub-groupings within a section, lighter visual weight
Tabs When sections are numerous and would cause scrolling
Grid Column layout within sections (not a replacement for sections)
Complex Form Example
public static function form(Form $form): Form { return $form ->schema([ Section::make('Product Details') ->icon('heroicon-o-cube') ->schema([ Fieldset::make('Basic Information') ->schema([ TextInput::make('name') ->required() ->maxLength(255), TextInput::make('sku') ->required() ->unique(ignoreRecord: true), ]) ->columns(2),
Fieldset::make('Pricing')
->schema([
TextInput::make('price')
->numeric()
->prefix('$')
->required(),
TextInput::make('compare_at_price')
->numeric()
->prefix('$'),
])
->columns(2),
RichEditor::make('description')
->columnSpanFull(),
]),
Section::make('Inventory')
->icon('heroicon-o-archive-box')
->collapsible()
->schema([
TextInput::make('quantity')
->numeric()
->default(0),
Toggle::make('track_inventory')
->default(true),
]),
Section::make('Media')
->icon('heroicon-o-photo')
->collapsible()
->collapsed()
->schema([
FileUpload::make('images')
->multiple()
->image()
->reorderable(),
]),
]);
}
Complete Field Reference
Text Input Fields
// Basic text input TextInput::make('name') ->required() ->maxLength(255) ->placeholder('Enter name...') ->helperText('This will be displayed publicly') ->prefixIcon('heroicon-o-user');
// Email input TextInput::make('email') ->email() ->required() ->unique(ignoreRecord: true);
// Password input TextInput::make('password') ->password() ->required() ->confirmed() ->minLength(8);
// Numeric input TextInput::make('price') ->numeric() ->prefix('$') ->minValue(0) ->maxValue(10000) ->step(0.01);
// Phone input TextInput::make('phone') ->tel() ->telRegex('/^[+][(]{0,1}[0-9]{1,4}[)]{0,1}[-\s./0-9]$/');
// URL input TextInput::make('website') ->url() ->suffixIcon('heroicon-o-globe-alt');
Textarea Fields
// Basic textarea Textarea::make('description') ->rows(5) ->cols(20) ->minLength(10) ->maxLength(1000) ->columnSpanFull();
// Auto-resize textarea Textarea::make('content') ->autosize() ->columnSpanFull();
Rich Text Editors
// Rich editor RichEditor::make('content') ->toolbarButtons([ 'blockquote', 'bold', 'bulletList', 'codeBlock', 'h2', 'h3', 'italic', 'link', 'orderedList', 'redo', 'strike', 'underline', 'undo', ]) ->columnSpanFull();
// Markdown editor MarkdownEditor::make('content') ->toolbarButtons([ 'bold', 'bulletList', 'codeBlock', 'edit', 'italic', 'link', 'orderedList', 'preview', 'strike', ]) ->columnSpanFull();
Select Fields
// Basic select Select::make('status') ->options([ 'draft' => 'Draft', 'reviewing' => 'Reviewing', 'published' => 'Published', ]) ->default('draft') ->required();
// Searchable select Select::make('country') ->options(Country::pluck('name', 'id')) ->searchable() ->preload();
// Multiple select Select::make('tags') ->multiple() ->options(Tag::pluck('name', 'id')) ->searchable();
// BelongsTo relationship Select::make('author_id') ->relationship('author', 'name') ->searchable() ->preload() ->createOptionForm([ TextInput::make('name') ->required(), TextInput::make('email') ->email() ->required(), ]);
// BelongsToMany relationship Select::make('categories') ->relationship('categories', 'name') ->multiple() ->preload();
Boolean Fields
// Toggle switch Toggle::make('is_active') ->label('Active') ->default(true) ->onColor('success') ->offColor('danger');
// Checkbox Checkbox::make('terms_accepted') ->label('I accept the terms and conditions') ->required() ->accepted();
// Checkbox list CheckboxList::make('permissions') ->options([ 'create' => 'Create', 'read' => 'Read', 'update' => 'Update', 'delete' => 'Delete', ]) ->columns(2);
// Radio buttons Radio::make('plan') ->options([ 'basic' => 'Basic Plan', 'pro' => 'Pro Plan', 'enterprise' => 'Enterprise Plan', ]) ->descriptions([ 'basic' => 'Best for individuals', 'pro' => 'Best for small teams', 'enterprise' => 'Best for large organizations', ]) ->required();
Date and Time Fields
// Date picker DatePicker::make('birth_date') ->native(false) ->displayFormat('d/m/Y') ->maxDate(now()) ->closeOnDateSelection();
// DateTime picker DateTimePicker::make('published_at') ->native(false) ->displayFormat('d/m/Y H:i') ->seconds(false) ->timezone('America/New_York');
// Time picker TimePicker::make('start_time') ->native(false) ->seconds(false) ->minutesStep(15);
File Upload Fields
// Basic file upload FileUpload::make('attachment') ->disk('public') ->directory('attachments') ->acceptedFileTypes(['application/pdf', 'image/*']) ->maxSize(10240) ->downloadable() ->openable();
// Image upload with preview FileUpload::make('avatar') ->image() ->imageEditor() ->circleCropper() ->disk('public') ->directory('avatars') ->visibility('public');
// Multiple files FileUpload::make('gallery') ->multiple() ->reorderable() ->appendFiles() ->image() ->disk('public') ->directory('gallery');
// Spatie Media Library SpatieMediaLibraryFileUpload::make('images') ->collection('images') ->multiple() ->reorderable();
Complex Fields
// Repeater (HasMany inline editing) Repeater::make('items') ->relationship() ->schema([ TextInput::make('name') ->required(), TextInput::make('quantity') ->numeric() ->required(), TextInput::make('price') ->numeric() ->prefix('$'), ]) ->columns(3) ->defaultItems(1) ->addActionLabel('Add Item') ->reorderable() ->collapsible();
// Builder (flexible content) Builder::make('content') ->blocks([ Builder\Block::make('heading') ->schema([ TextInput::make('content') ->label('Heading') ->required(), Select::make('level') ->options([ 'h2' => 'H2', 'h3' => 'H3', 'h4' => 'H4', ]), ]), Builder\Block::make('paragraph') ->schema([ RichEditor::make('content') ->required(), ]), Builder\Block::make('image') ->schema([ FileUpload::make('url') ->image() ->required(), TextInput::make('alt') ->label('Alt text'), ]), ]) ->columnSpanFull();
// Key-Value pairs KeyValue::make('metadata') ->keyLabel('Property') ->valueLabel('Value') ->addActionLabel('Add Property') ->reorderable();
// Tags input TagsInput::make('tags') ->suggestions([ 'laravel', 'filament', 'php', ]) ->splitKeys(['Tab', ',']);
Hidden and Special Fields
// Hidden field Hidden::make('user_id') ->default(auth()->id());
// Placeholder (display only) Placeholder::make('created_at') ->label('Created') ->content(fn ($record): string => $record?->created_at?->diffForHumans() ?? '-');
// View field (custom blade view) View::make('custom-field') ->view('filament.forms.custom-field');
Layout Components
Section
Section::make('Personal Information') ->description('Enter your personal details') ->icon('heroicon-o-user') ->collapsible() ->collapsed(false) ->schema([ // Fields ]);
Fieldset
Fieldset::make('Address') ->schema([ TextInput::make('street'), TextInput::make('city'), TextInput::make('state'), TextInput::make('zip'), ]) ->columns(2);
Tabs
Tabs::make('Tabs') ->tabs([ Tabs\Tab::make('General') ->icon('heroicon-o-information-circle') ->schema([ // General fields ]), Tabs\Tab::make('Media') ->icon('heroicon-o-photo') ->schema([ // Media fields ]), Tabs\Tab::make('SEO') ->icon('heroicon-o-magnifying-glass') ->schema([ // SEO fields ]), ]) ->columnSpanFull();
Grid and Columns
Grid::make() ->schema([ TextInput::make('first_name') ->columnSpan(1), TextInput::make('last_name') ->columnSpan(1), TextInput::make('email') ->columnSpanFull(), ]) ->columns(2);
Split Layout
Split::make([ Section::make('Main Content') ->schema([ // Primary fields ]), Section::make('Sidebar') ->schema([ // Secondary fields ]) ->grow(false), ]);
Validation
TextInput::make('email') ->email() ->required() ->unique(table: User::class, ignoreRecord: true) ->rules(['required', 'email', 'max:255']);
TextInput::make('slug') ->required() ->unique(ignoreRecord: true) ->rules([ fn (): Closure => function (string $attribute, $value, Closure $fail) { if (str_contains($value, ' ')) { $fail('Slug cannot contain spaces.'); } }, ]);
Conditional Visibility
Select::make('type') ->options([ 'individual' => 'Individual', 'company' => 'Company', ]) ->live();
TextInput::make('company_name') ->visible(fn (Get $get): bool => $get('type') === 'company');
TextInput::make('tax_id') ->hidden(fn (Get $get): bool => $get('type') !== 'company');
Output
Generated forms include:
-
Proper imports (including Section, Fieldset as needed)
-
Type declarations
-
Schema organized in Sections/Fieldsets (mandatory)
-
Validation rules
-
Layout structure with columns where appropriate
-
Relationship handling
-
Conditional logic
-
Collapsible sections for optional/secondary content