Filament PHP v5 Coding Standards
Comprehensive standards for building production-grade Filament v5 admin panels. Filament v5 requires PHP 8.3+, Laravel 13+, Livewire 4+, Tailwind CSS 4.1+. Follow these rules exactly. For detailed code examples, see REFERENCE.md.
When to Apply
Apply to ALL Filament work: resources, forms, tables, infolists, actions, widgets, dashboards, panels, relation managers, imports/exports, custom pages, multi-tenancy, testing, and deployment.
1. Project Structure
Directory Layout
- Domain-grouped, plural-named:
Resources/Shop/, Resources/Blog/, Resources/HR/
- Resources in plural directories:
Resources/Shop/Products/ProductResource.php
- Schemas extracted:
Resources/Shop/Products/Schemas/ProductSchema.php
- Tables extracted:
Resources/Shop/Products/Tables/ProductsTable.php
- Multiple panels:
Providers/Filament/AdminPanelProvider.php, AppPanelProvider.php
Resource Complexity Tiers
- Simple (ManageRecords) -- modal CRUD, single page, no relation managers
- Standard (List+Create+Edit) -- separate pages, basic CRUD
- Full (List+Create+Edit+View) -- sub-navigation, relation managers, widgets
Naming Rules
- Plural resource directories:
Products/, not Product/
- File names match class names:
ProductResource.php
- Slugs kebab-case:
order-items
- Namespace matches directory path exactly
2. Resource Architecture
Delegation Pattern
Resources delegate to dedicated Schema and Table classes. Keep resource classes slim.
class ProductResource extends Resource
{
protected static ?string $model = Product::class;
protected static ?string $slug = 'products';
protected static ?string $recordTitleAttribute = 'name';
protected static string|BackedEnum|null $navigationIcon = Heroicon::OutlinedShoppingBag;
protected static ?string $navigationGroup = 'Shop';
protected static ?int $navigationSort = 1;
public static function form(Schema $schema): Schema
{
return ProductSchema::configure($schema);
}
public static function table(Table $table): Table
{
return ProductsTable::configure($table);
}
}
Key Rules
- Always set
$recordTitleAttribute for global search
- Navigation icons: always
Heroicon::Outlined* (not Solid for nav)
- Use
Heroicon enum, never string icon names
- All actions import from
Filament\Actions\* namespace only
- Table methods:
recordActions() not actions(), groupedBulkActions() not bulkActions()
3. Form Design
- Top-level:
$schema->components([...]) with Section wrapping
- Section from
Filament\Schemas\Components\Section
- Layout components (Section, Grid, Tabs, Flex) from
Filament\Schemas\Components\*
- Slug fields:
->live(onBlur: true), create-only via Operation::Create, ->disabled()->dehydrated()
- Selects with relationships: always
->searchable() and ->preload()
- File uploads: set
->disk(), ->directory(), ->visibility()
- Use
Operation enum for conditional logic: Operation::Create, Operation::Edit
- Repeaters for line items with calculated fields
- Tabs for complex multi-section forms
- All text labels via language files, never hardcoded
4. Table Design
- Columns: TextColumn, IconColumn (booleans), BadgeColumn (enums), ImageColumn
- Toggleable columns:
->toggleable(isToggledHiddenByDefault: true) for less important columns
- Filters: SelectFilter, TernaryFilter (boolean), date range via custom filter
- Actions wrapped in
ActionGroup: View, Edit, Delete grouped
- Bulk actions via
groupedBulkActions(): delete, export, status change
- Toolbar actions via
toolbarActions(): create, import, export
- Default sort:
->defaultSort('created_at', 'desc')
- Searchable columns:
->searchable() on key text columns
- Money:
->money('USD') or ->formatStateUsing() for custom formatting
5. Infolist (View) Patterns
- Entry types: TextEntry, IconEntry (boolean), ImageEntry, BadgeEntry (enum)
- Inline labels for compact display:
->inlineLabel()
- Rich content:
->prose()->markdown() for formatted text
- Tabs for complex view pages
- Contextual actions on view pages with visibility conditions
- Key-value entries for metadata
6. Enum Design System
Every status/type/category uses a PHP 8.1 backed string enum implementing Filament contracts:
HasLabel -- display name
HasColor -- semantic color
HasIcon -- Heroicon
Semantic colors: success (positive), danger (negative), warning (caution), info (new), primary (special), gray (neutral)
Naming: PascalCase case names, snake_case backed values. Cast in model: 'status' => OrderStatus::class
7. Actions & Notifications
- All actions:
Filament\Actions\Action, Filament\Actions\CreateAction, etc.
- Never import from
Filament\Tables\Actions\* (removed in v5)
- Action modals use
->schema() not ->form()
- ActionGroup ordering: View, Edit, Delete (most common first)
- Notifications:
Notification::make()->title()->success()->send()
- Refresh after action:
$this->dispatch('$refresh') or return redirect
- Bulk actions:
->authorizeIndividualRecords() for policy-gated bulk operations
8. Relation Managers
- Standard: extend
RelationManager, define form() and table()
- Alternative:
ManageRelatedRecords page for complex relations
- Action placement:
headerActions (create), recordActions (per row), groupedBulkActions (selected)
- Reuse across resources when the same relation appears in multiple places
- Default sort on related records
9. Widgets & Dashboards
- StatsOverview: multiple stat cards, optional inline charts, descriptions, icons
- Chart widgets: line, bar, doughnut, pie -- extend
ChartWidget
- Lazy loading: on by default (
$isLazy = true)
- Polling: default 5s, customize via
$pollingInterval or null to disable
- Dashboard filters: implement
HasFiltersForm for filterable dashboards
- Resource page widgets: use
ExposesTableToWidgets trait
- Widget sort order via
$sort property
10. Multi-Tenancy
- Panel:
->tenant(Team::class) in PanelProvider
- User model: implement
HasTenants with getTenants() and canAccessTenant()
- Automatic query scoping on all resources (opt-out with
$isScopedToTenant = false)
- CRITICAL: Form selects are NOT auto-scoped -- add
modifyQueryUsing with Filament::getTenant()
- Validation: use
->scopedUnique() and ->scopedExists() for tenant-aware rules
- Domain-based:
->tenantDomain('{tenant:slug}.example.com')
- Path-based with slug:
->tenant(Team::class, slugAttribute: 'slug')
- Registration page: extend
RegisterTenant
- Profile page: extend
EditTenantProfile
11. Multi-Panel Architecture
- Separate PanelProvider per panel (admin, app, vendor)
- Each panel: own auth, resources, pages, widgets, middleware, theme
FilamentUser::canAccessPanel() controls per-panel access
- Shared config via Plugin class or static helper
Filament::setCurrentPanel('app') for testing non-default panels
12. Custom Pages & Clusters
- Custom pages:
php artisan make:filament-page Settings
- Access control:
canAccess() method
- Header actions, header/footer widgets, widget data passing
- Clusters: group related pages under sub-navigation
SubNavigationPosition::Top for horizontal tabs
- Resource sub-navigation:
getRecordSubNavigation() for View/Edit/Related pages
13. Testing (Pest + Livewire)
- Test via
livewire(PageClass::class) -- pages are Livewire components
- Auth:
actingAs(User::factory()->create()) in beforeEach
- Multi-tenant:
Filament::setTenant($team) + Filament::bootCurrentPanel()
- Multi-panel:
Filament::setCurrentPanel('admin')
Key test patterns:
- List:
->assertCanSeeTableRecords(), ->searchTable(), ->sortTable(), ->filterTable()
- Create:
->fillForm([...])->call('create')->assertHasNoFormErrors()->assertNotified()
- Edit:
->assertSchemaStateSet([...])->fillForm([...])->call('save')
- Actions:
->callAction(TestAction::make('send')->table($record))
- Bulk:
->selectTableRecords($ids)->callAction(TestAction::make('delete')->table()->bulk())
- Relations:
livewire(RelationManager::class, ['ownerRecord' => $record, 'pageClass' => EditPage::class])
14. Import & Export
- Exporter:
ExportColumn definitions, placed in app/Filament/Exports/
- Importer:
ImportColumn with rules, examples, casting, relationship resolution
- Actions:
ExportAction::make(), ImportAction::make() in toolbar
- Record resolution:
firstOrNew, new Model, or null to skip
15. Authorization
FilamentUser interface required in production (APP_ENV != local)
- Model policies auto-discovered:
viewAny, create, update, delete, restore, forceDelete, reorder
- Skip authorization:
$shouldSkipAuthorization = true
- Bulk action authorization:
->authorizeIndividualRecords()
- Per-panel access:
canAccessPanel(Panel $panel) with panel ID check
16. Performance & Deployment
Production:
php artisan optimize
php artisan filament:optimize
Panel config:
->spa() for SPA mode
->unsavedChangesAlerts() for data protection
->databaseTransactions() for action safety
Widgets: Lazy by default. Set polling interval or null to disable. Use wire:init for deferred API loads.
Never run filament:optimize in local dev -- new components won't be discovered.
Tailwind v4: CSS-based config via Vite plugin. No tailwind.config.js. Use @source directives.
17. Livewire 4 Features (via Filament v5)
- Islands: isolated re-render regions with
@island directive
- Async actions:
#[Async] attribute for fire-and-forget (analytics, logging)
- wire:model.deep: required when relying on child element event bubbling
- wire:sort: built-in drag-and-drop without external packages
- Parallel requests:
wire:model.live no longer blocks
- Self-closing tags required:
<livewire:component />
18. Strict Rules
- Never publish Blade views. Use CSS hooks with
fi- prefix.
- Never hardcode brand names, logos, currency. Use dynamic settings.
- All user-facing text through language files and translation keys.
$recordTitleAttribute set on every resource.
- Action modals:
->schema() not ->form()
- Table:
recordActions() not actions(), groupedBulkActions() not bulkActions()
- Schema:
$schema->components([...]) at top level
- Icons:
Heroicon:: enum only, never string names
- Operation:
Operation::Create, Operation::Edit, never string comparisons
Summary Checklist
1---2name: olakunlevpn-filament-skills3description: Use when writing, reviewing, or refactoring Filament PHP v5 code -- resources, forms, tables, infolists, actions, widgets, panels, relation managers, multi-tenancy, testing, or plugin development. Always apply these standards to all Filament work.4---56# Filament PHP v5 Coding Standards78Comprehensive standards for building production-grade Filament v5 admin panels. Filament v5 requires PHP 8.3+, Laravel 13+, Livewire 4+, Tailwind CSS 4.1+. Follow these rules exactly. For detailed code examples, see REFERENCE.md.910## When to Apply1112Apply to ALL Filament work: resources, forms, tables, infolists, actions, widgets, dashboards, panels, relation managers, imports/exports, custom pages, multi-tenancy, testing, and deployment.1314---1516## 1. Project Structure1718### Directory Layout19- Domain-grouped, plural-named: `Resources/Shop/`, `Resources/Blog/`, `Resources/HR/`20- Resources in plural directories: `Resources/Shop/Products/ProductResource.php`21- Schemas extracted: `Resources/Shop/Products/Schemas/ProductSchema.php`22- Tables extracted: `Resources/Shop/Products/Tables/ProductsTable.php`23- Multiple panels: `Providers/Filament/AdminPanelProvider.php`, `AppPanelProvider.php`2425### Resource Complexity Tiers26- **Simple** (ManageRecords) -- modal CRUD, single page, no relation managers27- **Standard** (List+Create+Edit) -- separate pages, basic CRUD28- **Full** (List+Create+Edit+View) -- sub-navigation, relation managers, widgets2930### Naming Rules31- Plural resource directories: `Products/`, not `Product/`32- File names match class names: `ProductResource.php`33- Slugs kebab-case: `order-items`34- Namespace matches directory path exactly3536---3738## 2. Resource Architecture3940### Delegation Pattern41Resources delegate to dedicated Schema and Table classes. Keep resource classes slim.4243```php44class ProductResource extends Resource45{46 protected static ?string $model = Product::class;47 protected static ?string $slug = 'products';48 protected static ?string $recordTitleAttribute = 'name';49 protected static string|BackedEnum|null $navigationIcon = Heroicon::OutlinedShoppingBag;50 protected static ?string $navigationGroup = 'Shop';51 protected static ?int $navigationSort = 1;5253 public static function form(Schema $schema): Schema54 {55 return ProductSchema::configure($schema);56 }5758 public static function table(Table $table): Table59 {60 return ProductsTable::configure($table);61 }62}63```6465### Key Rules66- Always set `$recordTitleAttribute` for global search67- Navigation icons: always `Heroicon::Outlined*` (not Solid for nav)68- Use `Heroicon` enum, never string icon names69- All actions import from `Filament\Actions\*` namespace only70- Table methods: `recordActions()` not `actions()`, `groupedBulkActions()` not `bulkActions()`7172---7374## 3. Form Design7576- Top-level: `$schema->components([...])` with `Section` wrapping77- Section from `Filament\Schemas\Components\Section`78- Layout components (Section, Grid, Tabs, Flex) from `Filament\Schemas\Components\*`79- Slug fields: `->live(onBlur: true)`, create-only via `Operation::Create`, `->disabled()->dehydrated()`80- Selects with relationships: always `->searchable()` and `->preload()`81- File uploads: set `->disk()`, `->directory()`, `->visibility()`82- Use `Operation` enum for conditional logic: `Operation::Create`, `Operation::Edit`83- Repeaters for line items with calculated fields84- Tabs for complex multi-section forms85- All text labels via language files, never hardcoded8687---8889## 4. Table Design9091- Columns: TextColumn, IconColumn (booleans), BadgeColumn (enums), ImageColumn92- Toggleable columns: `->toggleable(isToggledHiddenByDefault: true)` for less important columns93- Filters: SelectFilter, TernaryFilter (boolean), date range via custom filter94- Actions wrapped in `ActionGroup`: View, Edit, Delete grouped95- Bulk actions via `groupedBulkActions()`: delete, export, status change96- Toolbar actions via `toolbarActions()`: create, import, export97- Default sort: `->defaultSort('created_at', 'desc')`98- Searchable columns: `->searchable()` on key text columns99- Money: `->money('USD')` or `->formatStateUsing()` for custom formatting100101---102103## 5. Infolist (View) Patterns104105- Entry types: TextEntry, IconEntry (boolean), ImageEntry, BadgeEntry (enum)106- Inline labels for compact display: `->inlineLabel()`107- Rich content: `->prose()->markdown()` for formatted text108- Tabs for complex view pages109- Contextual actions on view pages with visibility conditions110- Key-value entries for metadata111112---113114## 6. Enum Design System115116Every status/type/category uses a PHP 8.1 backed string enum implementing Filament contracts:117- `HasLabel` -- display name118- `HasColor` -- semantic color119- `HasIcon` -- Heroicon120121**Semantic colors:** success (positive), danger (negative), warning (caution), info (new), primary (special), gray (neutral)122123**Naming:** PascalCase case names, snake_case backed values. Cast in model: `'status' => OrderStatus::class`124125---126127## 7. Actions & Notifications128129- All actions: `Filament\Actions\Action`, `Filament\Actions\CreateAction`, etc.130- Never import from `Filament\Tables\Actions\*` (removed in v5)131- Action modals use `->schema()` not `->form()`132- ActionGroup ordering: View, Edit, Delete (most common first)133- Notifications: `Notification::make()->title()->success()->send()`134- Refresh after action: `$this->dispatch('$refresh')` or return redirect135- Bulk actions: `->authorizeIndividualRecords()` for policy-gated bulk operations136137---138139## 8. Relation Managers140141- Standard: extend `RelationManager`, define `form()` and `table()`142- Alternative: `ManageRelatedRecords` page for complex relations143- Action placement: `headerActions` (create), `recordActions` (per row), `groupedBulkActions` (selected)144- Reuse across resources when the same relation appears in multiple places145- Default sort on related records146147---148149## 9. Widgets & Dashboards150151- **StatsOverview**: multiple stat cards, optional inline charts, descriptions, icons152- **Chart widgets**: line, bar, doughnut, pie -- extend `ChartWidget`153- **Lazy loading**: on by default (`$isLazy = true`)154- **Polling**: default 5s, customize via `$pollingInterval` or `null` to disable155- **Dashboard filters**: implement `HasFiltersForm` for filterable dashboards156- **Resource page widgets**: use `ExposesTableToWidgets` trait157- Widget sort order via `$sort` property158159---160161## 10. Multi-Tenancy162163- Panel: `->tenant(Team::class)` in PanelProvider164- User model: implement `HasTenants` with `getTenants()` and `canAccessTenant()`165- Automatic query scoping on all resources (opt-out with `$isScopedToTenant = false`)166- **CRITICAL**: Form selects are NOT auto-scoped -- add `modifyQueryUsing` with `Filament::getTenant()`167- Validation: use `->scopedUnique()` and `->scopedExists()` for tenant-aware rules168- Domain-based: `->tenantDomain('{tenant:slug}.example.com')`169- Path-based with slug: `->tenant(Team::class, slugAttribute: 'slug')`170- Registration page: extend `RegisterTenant`171- Profile page: extend `EditTenantProfile`172173---174175## 11. Multi-Panel Architecture176177- Separate PanelProvider per panel (admin, app, vendor)178- Each panel: own auth, resources, pages, widgets, middleware, theme179- `FilamentUser::canAccessPanel()` controls per-panel access180- Shared config via Plugin class or static helper181- `Filament::setCurrentPanel('app')` for testing non-default panels182183---184185## 12. Custom Pages & Clusters186187- Custom pages: `php artisan make:filament-page Settings`188- Access control: `canAccess()` method189- Header actions, header/footer widgets, widget data passing190- Clusters: group related pages under sub-navigation191- `SubNavigationPosition::Top` for horizontal tabs192- Resource sub-navigation: `getRecordSubNavigation()` for View/Edit/Related pages193194---195196## 13. Testing (Pest + Livewire)197198- Test via `livewire(PageClass::class)` -- pages are Livewire components199- Auth: `actingAs(User::factory()->create())` in `beforeEach`200- Multi-tenant: `Filament::setTenant($team)` + `Filament::bootCurrentPanel()`201- Multi-panel: `Filament::setCurrentPanel('admin')`202203**Key test patterns:**204- List: `->assertCanSeeTableRecords()`, `->searchTable()`, `->sortTable()`, `->filterTable()`205- Create: `->fillForm([...])->call('create')->assertHasNoFormErrors()->assertNotified()`206- Edit: `->assertSchemaStateSet([...])->fillForm([...])->call('save')`207- Actions: `->callAction(TestAction::make('send')->table($record))`208- Bulk: `->selectTableRecords($ids)->callAction(TestAction::make('delete')->table()->bulk())`209- Relations: `livewire(RelationManager::class, ['ownerRecord' => $record, 'pageClass' => EditPage::class])`210211---212213## 14. Import & Export214215- Exporter: `ExportColumn` definitions, placed in `app/Filament/Exports/`216- Importer: `ImportColumn` with rules, examples, casting, relationship resolution217- Actions: `ExportAction::make()`, `ImportAction::make()` in toolbar218- Record resolution: `firstOrNew`, `new Model`, or `null` to skip219220---221222## 15. Authorization223224- `FilamentUser` interface required in production (APP_ENV != local)225- Model policies auto-discovered: `viewAny`, `create`, `update`, `delete`, `restore`, `forceDelete`, `reorder`226- Skip authorization: `$shouldSkipAuthorization = true`227- Bulk action authorization: `->authorizeIndividualRecords()`228- Per-panel access: `canAccessPanel(Panel $panel)` with panel ID check229230---231232## 16. Performance & Deployment233234**Production:**235```236php artisan optimize237php artisan filament:optimize238```239240**Panel config:**241- `->spa()` for SPA mode242- `->unsavedChangesAlerts()` for data protection243- `->databaseTransactions()` for action safety244245**Widgets:** Lazy by default. Set polling interval or `null` to disable. Use `wire:init` for deferred API loads.246247**Never run `filament:optimize` in local dev** -- new components won't be discovered.248249**Tailwind v4:** CSS-based config via Vite plugin. No `tailwind.config.js`. Use `@source` directives.250251---252253## 17. Livewire 4 Features (via Filament v5)254255- **Islands**: isolated re-render regions with `@island` directive256- **Async actions**: `#[Async]` attribute for fire-and-forget (analytics, logging)257- **wire:model.deep**: required when relying on child element event bubbling258- **wire:sort**: built-in drag-and-drop without external packages259- **Parallel requests**: `wire:model.live` no longer blocks260- **Self-closing tags required**: `<livewire:component />`261262---263264## 18. Strict Rules265266- Never publish Blade views. Use CSS hooks with `fi-` prefix.267- Never hardcode brand names, logos, currency. Use dynamic settings.268- All user-facing text through language files and translation keys.269- `$recordTitleAttribute` set on every resource.270- Action modals: `->schema()` not `->form()`271- Table: `recordActions()` not `actions()`, `groupedBulkActions()` not `bulkActions()`272- Schema: `$schema->components([...])` at top level273- Icons: `Heroicon::` enum only, never string names274- Operation: `Operation::Create`, `Operation::Edit`, never string comparisons275276## Summary Checklist277278- [ ] Resource delegates to Schema and Table classes279- [ ] `$recordTitleAttribute` set on every resource280- [ ] `Heroicon::` enum for all icons, `Outlined` for navigation281- [ ] All actions from `Filament\Actions\*` namespace282- [ ] Table uses `recordActions()`, `groupedBulkActions()`, `toolbarActions()`283- [ ] Enums implement HasLabel, HasColor, HasIcon with semantic colors284- [ ] Form selects are searchable and preloaded285- [ ] Multi-tenant form selects manually scoped to tenant286- [ ] `FilamentUser` interface implemented with `canAccessPanel()`287- [ ] Model policies for authorization on every resource288- [ ] `filament:optimize` in production deploy script289- [ ] Tests use `livewire()` with `Filament::setTenant()` for multi-tenant290- [ ] No hardcoded text -- all through language files291- [ ] No published Blade views -- CSS hooks only