
Filament Pro
- 927 installs
- 15 repo stars
- Updated August 2, 2026
- marcelorodrigo/agent-skills
filament-pro is a Claude Code skill that generates complete Laravel admin panels with CRUD resources, forms, tables, widgets, and dashboards using Filament v5's declarative PHP API for developers who need production admi
About
filament-pro is a version 1.0.0 agent skill for building Laravel admin panels with Filament v5, Livewire v4, and TailwindCSS v4.1+. It guides agents through creating CRUD resources, form schemas, data tables, dashboard widgets, and admin interface tests using Filament's server-driven UI model—real-time reactivity without custom JavaScript. The skill requires Laravel 11.28+, PHP 8.2+, and encodes Filament v5 patterns including Schemas API conventions. Developers reach for filament-pro when scaffolding internal admin tools, back-office dashboards, or content management interfaces inside existing Laravel 11 applications. It accelerates repetitive Filament boilerplate—resource classes, relation managers, form fields, table columns—while keeping output aligned with official Filament v5 architecture.
- Generates full CRUD Resources for Eloquent models automatically
- Uses declarative Schemas for forms, tables, infolists and actions
- Built on Livewire v4 for real-time reactivity without JavaScript
- Includes PanelProvider, Widgets, and Actions with modal support
- Scaffolds complete directory structure and Tailwind v4 assets in one command
Filament Pro by the numbers
- 927 all-time installs (skills.sh)
- +26 installs in the week ending Aug 4, 2026 (Skillselion tracking)
- Ranked #12 of 65 PHP & Laravel skills by installs in the Skillselion catalog
- Security screen: MEDIUM risk (skills.sh audit)
- Data as of Aug 4, 2026 (Skillselion catalog sync)
npx skills add https://github.com/marcelorodrigo/agent-skills --skill filament-proAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 927 |
|---|---|
| repo stars | ★ 15 |
| Security audit | 2 / 3 scanners passed |
| Last updated | August 2, 2026 |
| Repository | marcelorodrigo/agent-skills ↗ |
How do you scaffold Filament v5 admin CRUD in Laravel?
Rapidly generate complete Laravel admin panels with CRUD resources, forms, tables, and dashboards using declarative PHP.
Who is it for?
Laravel developers on PHP 8.2+ and Laravel 11.28+ who need Filament v5 admin panels with CRUD, forms, and tables generated quickly.
Skip if: Developers building non-Laravel stacks, frontend-only SPAs, or admin UIs outside the Filament/Livewire ecosystem.
When should I use this skill?
A developer asks to create Filament resources, admin CRUD panels, form schemas, table columns, or dashboard widgets in a Laravel project.
What you get
Filament Resource classes, form schemas, table definitions, dashboard widgets, and Livewire v4 admin panel routes.
- Filament Resource classes
- Form and table schemas
- Dashboard widgets
By the numbers
- Skill version 1.0.0
- Requires Laravel 11.28+, PHP 8.2+, Livewire v4, TailwindCSS v4.1+
Files
Filament v5
Build powerful Laravel admin panels using Filament v5's server-driven UI with Schemas and Livewire v4 reactivity.
Overview
Filament v5 is a Laravel admin panel framework that provides complete CRUD interfaces, forms, tables, and dashboard components through a declarative PHP API. Built on Livewire v4, it offers real-time reactivity without writing JavaScript.
Key Concepts
- PanelProvider: Central configuration class defining your admin panel
- Resources: Automatic CRUD interfaces for Eloquent models
- Schemas: Declarative UI components (forms, tables, infolists)
- Actions: Interactive buttons with modals and backend logic
- Widgets: Dashboard components for data visualization
System Requirements
- Laravel 11.28+
- PHP 8.2+
- Livewire v4
- Node.js 18+
- Tailwind CSS v4.1+
Installation
Install Filament via Composer and scaffold a panel:
composer require filament/filament:"^5.0" -W
php artisan filament:install --scaffold
npm install && npm run dev
php artisan make:filament-userThis creates the panel provider, directory structure, and assets needed to start building.
Directory Structure
app/
Filament/
Resources/ # CRUD resources with forms and tables
Pages/ # Custom pages
Widgets/ # Dashboard widgets
Providers/
Filament/
AdminPanelProvider.phpCore Concepts
Panel Configuration
The PanelProvider is the entry point for your admin panel. It configures:
- Identity: ID, path, branding (name, logo, colors)
- Discovery: Auto-discovery of resources, pages, and widgets
- Middleware: Session, authentication, and custom middleware
- Tenancy: Multi-tenant configuration for SaaS applications
Resources
Resources provide complete CRUD interfaces through:
- Forms: Schema-based forms with 20+ field types (TextInput, Select, DatePicker, FileUpload, RichEditor, etc.)
- Tables: Data tables with columns, filters, sorting, and actions
- Pages: Automatic generation of List, Create, Edit, and View pages
- Relations: Relation managers for handling model relationships
Forms
Forms use a schema-based approach where you declare fields as PHP objects:
- Input Fields: Text, select, checkbox, toggle, date/time pickers
- Media: File and image uploads with validation
- Complex Fields: Rich text editors, repeaters, builders
- Layout: Grids, sections, tabs, and wizards
- Validation: Built-in Laravel validation rules
Tables
Tables display data with extensive customization:
- Columns: Text, badges, icons, images, colors
- Filters: Select, ternary, and custom filter logic
- Actions: Per-row actions, bulk actions, header actions
- Features: Search, sorting, pagination, grouping
Actions
Actions are interactive buttons that trigger:
- Modals: Form dialogs for data collection
- Confirmation: Destructive action confirmation
- Wizards: Multi-step processes
- Notifications: User feedback after completion
Widgets
Dashboard widgets include:
- Stats Overview: Metric cards with trends and sparklines
- Charts: Line, bar, pie charts using Chart.js
- Tables: Data tables for recent records
Testing
Filament uses Pest PHP with Livewire testing helpers:
- Page Testing: List, create, edit, view page functionality
- Form Testing: Validation, state management, submission
- Table Testing: Search, filters, sorting, actions
- Authorization Testing: Access control and permissions
Authorization
Access control through:
- Panel Access: FilamentUser contract for panel-level access
- Policies: Laravel policies for resource-level permissions
- Field Visibility: Show/hide fields based on user roles
- Multi-Tenancy: Tenant isolation for SaaS applications
Architecture Patterns
Server-Driven UI
Filament uses a server-driven approach where the backend defines the UI structure through schemas. The PHP code describes forms, tables, and layouts which Filament renders as Livewire components.
Schema System
Schemas are PHP configuration objects that define:
- Form fields and their validation rules
- Table columns and their formatting
- Layout containers (grids, sections, tabs)
- Action definitions and their behavior
Livewire Integration
All components mount as Livewire components, providing:
- Real-time reactivity without page reloads
- Automatic state management
- Event handling and AJAX updates
- Form validation with instant feedback
Resource-First Design
The framework encourages a resource-first approach: 1. Define your Eloquent models 2. Create resources that map to those models 3. Configure forms and tables for each resource 4. Add actions and widgets as needed
Command Reference
| Command | Purpose |
|---|---|
filament:install --scaffold | Install Filament with panel scaffolding |
make:filament-resource | Create CRUD resource |
make:filament-page | Create custom page |
make:filament-widget | Create dashboard widget |
make:filament-panel | Create additional panel |
make:filament-user | Create admin user |
make:filament-relation-manager | Create relation manager |
filament:cache-components | Cache for production |
Detailed Documentation
Reference Guides
Comprehensive documentation for each component:
- [Forms](references/forms.md) - All form components, validation rules, layouts, and conditional logic
- [Tables](references/tables.md) - Column types, filters, actions, and table configuration
- [Resources](references/resources.md) - CRUD resources, relation managers, infolists, and global search
- [Infolists](references/infolists.md) - Read-only data display components (TextEntry, ImageEntry, IconEntry)
- [Widgets](references/widgets.md) - Stats overview, charts, and table widgets
- [Actions](references/actions.md) - Modal actions, notifications, action groups, and wizards
- [Notifications](references/notifications.md) - Flash messages, database, and broadcast notifications
- [Schemas](references/schemas.md) - Schema system, layouts, and component organization
- [Testing](references/testing.md) - Pest testing patterns for resources, forms, tables, and authorization
- [Authorization](references/authorization.md) - Access control, policies, roles, and multi-tenancy
Code Examples
See examples.md for complete working code examples including:
- Complete resource implementations
- Form configurations
- Table setups
- Widget configurations
- Test suites
- Authorization patterns
Best Practices
Performance
- Use
getEloquentQuery()to eager load relationships and prevent N+1 queries - Enable component caching in production with
filament:cache-components - Limit pagination options and use deferred loading for large datasets
- Cache expensive calculations in widgets
Security
- Always implement the FilamentUser contract for panel access control
- Use Laravel policies for resource-level authorization
- Validate all input with appropriate form rules
- Never skip authorization in production environments
- Implement proper tenant isolation for multi-tenant applications
Code Organization
- Organize by feature:
app/Filament/Admin/Resources/ - Extract complex forms and tables to separate classes
- Create reusable form components for common patterns
- Keep resources focused on single responsibility
- Use dedicated pages for non-CRUD functionality
Testing
- Test all CRUD operations for each resource
- Validate form validation rules with multiple scenarios
- Test table features: search, filters, sorting, actions
- Verify authorization with different user roles
- Use factories to create realistic test data
When to Use Filament
Filament is ideal for:
- Admin Panels: Back-office interfaces for managing application data
- CMS: Content management systems with rich editing capabilities
- CRM: Customer relationship management tools
- E-commerce: Product, order, and inventory management
- SaaS Applications: Multi-tenant admin interfaces
- Internal Tools: Business process management and data entry
Additional Resources
---
Version: 1.0.0 License: MIT Compatibility: Laravel 11+, PHP 8.2+, Livewire v4
Actions Reference
Complete guide for creating actions, modals, and notifications in Filament v5.
Action Types
| Action | Class | Description |
|---|---|---|
| Create | CreateAction | Create a new record |
| Edit | EditAction | Edit existing record |
| View | ViewAction | View record details |
| Delete | DeleteAction | Delete a record |
| Replicate | ReplicateAction | Duplicate a record |
| Restore | RestoreAction | Restore soft-deleted record |
| Force Delete | ForceDeleteAction | Permanently delete |
| Import | ImportAction | Import from file |
| Export | ExportAction | Export to file |
Basic Actions
use Filament\Actions\Action;
// Simple action
Action::make('save')
->label('Save Changes')
->icon('heroicon-m-check')
->color('primary')
->action(fn () => $this->save())
// URL action
Action::make('visit')
->label('Visit Website')
->icon('heroicon-m-arrow-top-right-on-square')
->url(fn (Post $record) => $record->website)
->openUrlInNewTab()
// Hidden action
Action::make('publish')
->hidden(fn (Post $record): bool => $record->isPublished())
// Disabled action
Action::make('delete')
->disabled(fn (Post $record): bool => ! auth()->user()->can('delete', $record))
// Visible action
Action::make('edit')
->visible(fn (Post $record): bool => auth()->user()->can('edit', $record))Action Configuration
Icons and Colors
Action::make('approve')
->icon('heroicon-m-check-circle')
->iconPosition('before') // or 'after'
->iconSize('sm') // 'sm', 'md', 'lg'
->color('success') // primary, success, danger, warning, info, gray
->outlined() // Outlined button style
->link() // Link styleLabels and Tooltips
Action::make('delete')
->label('Delete Record')
->tooltip('Permanently delete this record')
->helperText('This action cannot be undone')Authorization
Action::make('edit')
->authorize('update', $this->post)
->authorize(fn (): bool => auth()->user()->can('update', $this->post))Modal Actions
Basic Modal
use Filament\Forms\Components\TextInput;
use Filament\Forms\Components\Textarea;
Action::make('sendEmail')
->icon('heroicon-m-envelope')
->form([
TextInput::make('subject')
->required()
->maxLength(255),
Textarea::make('body')
->required()
->columnSpanFull(),
])
->action(function (array $data, Post $record) {
Mail::to($record->author->email)
->send(new PostNotification($data['subject'], $data['body']));
})
->successNotification(
Notification::make()
->title('Email sent')
->success()
)Confirmation Modal
Action::make('delete')
->color('danger')
->icon('heroicon-m-trash')
->requiresConfirmation()
->modalHeading('Delete post')
->modalDescription('Are you sure you want to delete this post? This action cannot be undone.')
->modalSubmitActionLabel('Yes, delete it')
->modalCancelActionLabel('Cancel')
->action(fn (Post $record) => $record->delete())Modal Customization
Action::make('edit')
->modalHeading('Edit Customer')
->modalDescription('Update customer information')
->modalWidth(MaxWidth::Large) // Small, Medium, Large, ExtraLarge, TwoExtraLarge, ThreeExtraLarge, FourExtraLarge, FiveExtraLarge, Screen
->modalIcon('heroicon-m-pencil-square')
->modalIconColor('primary')
->modalFooterActionsAlignment(Alignment::Right)
->modalAutofocus()
->closeModalByClickingAway(false)
->closeModalByPressingEscape(false)Wizard Modal
use Filament\Forms\Components\Wizard;
Action::make('createOrder')
->steps([
Wizard\Step::make('Customer')
->icon('heroicon-m-user')
->description('Select customer')
->schema([
Forms\Components\Select::make('customer_id')
->relationship('customer', 'name')
->searchable()
->required(),
]),
Wizard\Step::make('Products')
->icon('heroicon-m-shopping-bag')
->description('Add products')
->schema([
Forms\Components\Repeater::make('items')
->schema([
Forms\Components\Select::make('product_id')
->relationship('product', 'name')
->searchable()
->required(),
Forms\Components\TextInput::make('quantity')
->numeric()
->required()
->default(1),
])
->columns(2),
]),
Wizard\Step::make('Review')
->icon('heroicon-m-eye')
->description('Review order')
->schema([
Forms\Components\Placeholder::make('summary')
->content('Review your order before submitting.'),
]),
])
->action(function (array $data) {
Order::create($data);
})
->modalWidth(MaxWidth::ExtraLarge)Notifications
Basic Notifications
use Filament\Notifications\Notification;
Notification::make()
->title('Saved successfully')
->success()
->send();
Notification::make()
->title('Error occurred')
->body('Unable to save the record.')
->danger()
->send();
Notification::make()
->title('Warning')
->body('This action cannot be undone.')
->warning()
->persistent()
->send();
Notification::make()
->title('Information')
->body('New updates are available.')
->info()
->send();Notification with Actions
Notification::make()
->title('Order placed successfully')
->success()
->body('Your order #12345 has been confirmed.')
->actions([
Notification\Action::make('view')
->button()
->url('/orders/12345')
->openUrlInNewTab(),
Notification\Action::make('undo')
->color('gray')
->close()
->action(function () {
// Undo logic
}),
])
->send();Notification to Specific User
Notification::make()
->title('New comment')
->body('Someone commented on your post.')
->sendToDatabase($user);Notification Icons
Notification::make()
->title('Success!')
->icon('heroicon-o-check-circle')
->iconColor('success')
->success()
->send();Action Groups
use Filament\Actions\ActionGroup;
ActionGroup::make([
Action::make('view')
->icon('heroicon-m-eye')
->url(fn (Post $record): string => route('posts.show', $record)),
Action::make('edit')
->icon('heroicon-m-pencil-square')
->url(fn (Post $record): string => route('posts.edit', $record)),
Action::make('duplicate')
->icon('heroicon-m-document-duplicate')
->action(fn (Post $record) => $record->replicate()->save()),
Action::make('delete')
->icon('heroicon-m-trash')
->color('danger')
->requiresConfirmation()
->action(fn (Post $record) => $record->delete()),
])
->label('Actions')
->icon('heroicon-m-ellipsis-vertical')
->color('gray')
->button() // Render as button group
->tooltip('More actions')Bulk Actions
use Filament\Actions\BulkAction;
use Filament\Actions\BulkActionGroup;
->bulkActions([
BulkActionGroup::make([
DeleteBulkAction::make(),
BulkAction::make('publish')
->icon('heroicon-m-check-circle')
->color('success')
->requiresConfirmation()
->action(fn (Collection $records) =>
$records->each->update(['status' => 'published'])
),
BulkAction::make('changeStatus')
->icon('heroicon-m-pencil-square')
->form([
Select::make('status')
->options([
'draft' => 'Draft',
'published' => 'Published',
'archived' => 'Archived',
])
->required(),
])
->action(fn (Collection $records, array $data) =>
$records->each->update(['status' => $data['status']])
),
BulkAction::make('export')
->icon('heroicon-m-arrow-down-tray')
->action(function (Collection $records) {
$csv = $records->toCsv();
return response()->streamDownload(function () use ($csv) {
echo $csv;
}, 'export.csv');
}),
]),
])Header Actions
protected function getHeaderActions(): array
{
return [
Action::make('create')
->label('New Post')
->icon('heroicon-m-plus')
->url(fn (): string => route('posts.create')),
Action::make('import')
->icon('heroicon-m-arrow-up-tray')
->form([
FileUpload::make('file')
->acceptedFileTypes(['text/csv'])
->required(),
])
->action(function (array $data) {
// Import logic
Notification::make()
->title('Import complete')
->success()
->send();
}),
Action::make('settings')
->icon('heroicon-m-cog-6-tooth')
->url('/admin/settings'),
];
}Page Actions
In Resource Pages
// Create page
protected function getHeaderActions(): array
{
return [
Actions\CreateAction::make(),
];
}
// Edit page
protected function getHeaderActions(): array
{
return [
Actions\ViewAction::make(),
Actions\DeleteAction::make(),
];
}
// View page
protected function getHeaderActions(): array
{
return [
Actions\EditAction::make(),
Actions\DeleteAction::make(),
];
}
// List page
protected function getHeaderActions(): array
{
return [
Actions\CreateAction::make(),
];
}Action Hooks
Action::make('delete')
->before(function () {
// Run before action
Log::info('Deleting record...');
})
->action(function (Post $record) {
$record->delete();
})
->after(function () {
// Run after action
Notification::make()
->title('Deleted successfully')
->success()
->send();
})Complex Action Example
Action::make('processRefund')
->icon('heroicon-m-arrow-uturn-left')
->color('warning')
->requiresConfirmation()
->modalHeading('Process Refund')
->modalDescription('Are you sure you want to refund this order?')
->modalSubmitActionLabel('Yes, refund')
->modalWidth(MaxWidth::Large)
->form([
Section::make('Refund Details')
->schema([
TextInput::make('amount')
->numeric()
->required()
->prefix('$')
->maxValue(fn (Order $record) => $record->total),
Select::make('reason')
->options([
'customer_request' => 'Customer Request',
'damaged_item' => 'Damaged Item',
'wrong_item' => 'Wrong Item',
'other' => 'Other',
])
->required(),
Textarea::make('notes')
->columnSpanFull(),
]),
])
->action(function (array $data, Order $record) {
// Process refund
$refund = $record->refunds()->create([
'amount' => $data['amount'],
'reason' => $data['reason'],
'notes' => $data['notes'],
'processed_by' => auth()->id(),
]);
// Update order status
$record->update([
'status' => 'refunded',
'refunded_amount' => $record->refunded_amount + $data['amount'],
]);
// Send notification
Notification::make()
->title('Refund processed')
->body("Refund #{$refund->id} for {$data['amount']} has been processed.")
->success()
->actions([
Notification\Action::make('view')
->button()
->url("/admin/refunds/{$refund->id}"),
])
->send();
})
->visible(fn (Order $record): bool =>
$record->status === 'completed' &&
$record->refunded_amount < $record->total
)
->authorize(fn (Order $record): bool =>
auth()->user()->can('process refunds')
)Import Action
use Filament\Actions\ImportAction;
use Filament\Forms\Components\FileUpload;
Action::make('import')
->icon('heroicon-m-arrow-up-tray')
->form([
FileUpload::make('file')
->acceptedFileTypes([
'application/vnd.ms-excel',
'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
'text/csv',
])
->required(),
])
->action(function (array $data) {
$import = new ProductsImport();
Excel::import($import, $data['file']);
Notification::make()
->title('Import complete')
->body("{$import->getRowCount()} products imported successfully.")
->success()
->send();
})Export Action
use Filament\Actions\ExportAction;
Action::make('export')
->icon('heroicon-m-arrow-down-tray')
->form([
Select::make('format')
->options([
'csv' => 'CSV',
'xlsx' => 'Excel',
'pdf' => 'PDF',
])
->default('csv'),
Toggle::make('include_headers')
->default(true),
])
->action(function (array $data) {
$format = $data['format'];
$filename = "products-{$format}." . ($format === 'xlsx' ? 'xlsx' : $format);
return match ($format) {
'csv' => response()->streamDownload(fn () => Product::toCsv(), $filename),
'xlsx' => Excel::download(new ProductsExport(), $filename),
'pdf' => PDF::loadView('exports.products', ['products' => Product::all()])->download($filename),
};
})Best Practices
1. Use appropriate colors - Danger for destructive, Success for positive 2. Always confirm destructive actions - Use requiresConfirmation() 3. Provide clear labels - Action should describe what it does 4. Use icons - Enhance visual recognition 5. Handle errors gracefully - Catch exceptions and show notifications 6. Authorize actions - Check permissions before allowing 7. Give feedback - Always send notifications after actions 8. Use action groups - Group related actions together 9. Customize modals - Set appropriate headings and descriptions 10. Optimize bulk actions - Use database transactions for efficiency 11. Show loading states - For long-running actions 12. Log important actions - For audit trails 13. Test edge cases - Empty data, large datasets, errors 14. Use success notifications - Confirm actions completed 15. Keep actions focused - One action should do one thing
Tips & Tricks
Action with Loading State
Action::make('sync')
->action(function () {
// Long-running operation
$this->syncData();
})
->requiresConfirmation()
->modalHeading('Sync Data')
->modalDescription('This may take a few minutes...')Dynamic Action Label
Action::make('toggleStatus')
->label(fn (Post $record): string => $record->is_published ? 'Unpublish' : 'Publish')
->icon(fn (Post $record): string => $record->is_published ? 'heroicon-m-x-circle' : 'heroicon-m-check-circle')
->color(fn (Post $record): string => $record->is_published ? 'danger' : 'success')
->action(fn (Post $record) => $record->update(['is_published' => ! $record->is_published]))Conditional Confirmation
Action::make('delete')
->requiresConfirmation(fn (Post $record): bool => $record->comments()->count() > 0)
->modalHeading('Delete Post')
->modalDescription(fn (Post $record): string =>
$record->comments()->count() > 0
? "This post has {$record->comments()->count()} comments. Are you sure?"
: 'Are you sure you want to delete this post?'
)Additional Resources
Authorization Reference
Complete guide for access control, policies, and multi-tenancy in Filament v5.
Panel Access Control
FilamentUser Contract
Implement the FilamentUser contract on your User model to control panel access:
<?php
namespace App\Models;
use Filament\Models\Contracts\FilamentUser;
use Filament\Panel;
use Illuminate\Foundation\Auth\User as Authenticatable;
class User extends Authenticatable implements FilamentUser
{
// Simple check
public function canAccessPanel(Panel $panel): bool
{
return $this->hasVerifiedEmail();
}
// Panel-specific checks
public function canAccessPanel(Panel $panel): bool
{
return match ($panel->getId()) {
'admin' => $this->isAdmin() && $this->hasVerifiedEmail(),
'app' => $this->hasVerifiedEmail(),
'vendor' => $this->isVendor() && $this->hasVerifiedEmail(),
default => false,
};
}
// Domain-based access
public function canAccessPanel(Panel $panel): bool
{
return str_ends_with($this->email, '@company.com') &&
$this->hasVerifiedEmail();
}
}Laravel Policies
Filament automatically uses Laravel policies for authorization.
Creating Policies
php artisan make:policy PostPolicy --model=PostPolicy Structure
<?php
namespace App\Policies;
use App\Models\Post;
use App\Models\User;
use Illuminate\Auth\Access\Response;
class PostPolicy
{
public function viewAny(User $user): bool
{
return true;
}
public function view(User $user, Post $post): bool
{
return $user->id === $post->user_id || $user->isAdmin();
}
public function create(User $user): bool
{
return $user->can('create posts');
}
public function update(User $user, Post $post): bool|Response
{
if ($post->isLocked()) {
return Response::deny('This post is locked and cannot be edited.');
}
return $user->id === $post->user_id || $user->isAdmin();
}
public function delete(User $user, Post $post): bool
{
return $user->isAdmin() ||
($user->id === $post->user_id && $post->status === 'draft');
}
public function deleteAny(User $user): bool
{
return $user->isAdmin();
}
public function restore(User $user, Post $post): bool
{
return $user->isAdmin();
}
public function forceDelete(User $user, Post $post): bool
{
return $user->isAdmin();
}
// Custom policy methods
public function publish(User $user, Post $post): bool
{
return $user->can('publish posts') &&
$post->status === 'draft';
}
public function feature(User $user, Post $post): bool
{
return $user->isAdmin() || $user->isEditor();
}
}Registering Policies
<?php
namespace App\Providers;
use App\Models\Post;
use App\Policies\PostPolicy;
use Illuminate\Foundation\Support\Providers\AuthServiceProvider as ServiceProvider;
class AuthServiceProvider extends ServiceProvider
{
protected $policies = [
Post::class => PostPolicy::class,
];
public function boot(): void
{
//
}
}Resource Authorization
Automatic Policy Enforcement
By default, Filament checks policies automatically:
class PostResource extends Resource
{
protected static bool $shouldSkipAuthorization = false; // Default
}Manual Authorization Methods
class PostResource extends Resource
{
public static function canViewAny(): bool
{
return auth()->user()->can('viewAny', Post::class);
}
public static function canCreate(): bool
{
return auth()->user()->can('create', Post::class);
}
public static function canEdit(Model $record): bool
{
return auth()->user()->can('update', $record);
}
public static function canDelete(Model $record): bool
{
return auth()->user()->can('delete', $record);
}
public static function canView(Model $record): bool
{
return auth()->user()->can('view', $record);
}
public static function canRestore(Model $record): bool
{
return auth()->user()->can('restore', $record);
}
public static function canForceDelete(Model $record): bool
{
return auth()->user()->can('forceDelete', $record);
}
}Eloquent Query Scoping
public static function getEloquentQuery(): Builder
{
return parent::getEloquentQuery()
->where(function (Builder $query) {
if (! auth()->user()->isAdmin()) {
$query->where('user_id', auth()->id());
}
});
}Page Authorization
Custom Pages
<?php
namespace App\Filament\Pages;
use Filament\Pages\Page;
class Reports extends Page
{
protected static ?string $navigationIcon = 'heroicon-o-chart-bar';
protected static string $view = 'filament.pages.reports';
public static function canAccess(): bool
{
return auth()->user()->can('view reports');
}
// Or with dependencies
public static function canAccess(array $parameters = []): bool
{
return auth()->user()->can('view reports') &&
$parameters['type'] ?? false;
}
}Settings Pages
<?php
namespace App\Filament\Pages;
use Filament\Pages\Page;
class Settings extends Page
{
protected static ?string $navigationIcon = 'heroicon-o-cog';
protected static string $view = 'filament.pages.settings';
public static function canAccess(): bool
{
return auth()->user()->isAdmin();
}
}Action Authorization
Table Actions
->actions([
Tables\Actions\EditAction::make()
->authorize('update'),
Tables\Actions\DeleteAction::make()
->authorize('delete'),
Tables\Actions\Action::make('publish')
->icon('heroicon-m-check-circle')
->authorize('publish') // Calls PostPolicy::publish()
->visible(fn (Post $record): bool =>
auth()->user()->can('publish', $record)
)
->action(fn (Post $record) => $record->update(['status' => 'published'])),
])Page Actions
protected function getHeaderActions(): array
{
return [
Action::make('export')
->icon('heroicon-m-arrow-down-tray')
->authorize('export posts')
->action(function () {
// Export logic
}),
];
}Field-Level Authorization
Form Fields
public static function form(Form $form): Form
{
return $form
->schema([
TextInput::make('title')
->required(),
// Only visible to admins
TextInput::make('internal_notes')
->visible(fn (): bool => auth()->user()->isAdmin()),
// Only visible to users with permission
TextInput::make('reviewer_notes')
->visible(fn (): bool => auth()->user()->can('review posts')),
// Disabled for non-editors
Select::make('status')
->disabled(fn (): bool => !auth()->user()->can('publish posts')),
// Dehydrated (not saved) for certain roles
TextInput::make('debug_info')
->dehydrated(fn (): bool => auth()->user()->isDeveloper()),
]);
}Table Columns
public static function table(Table $table): Table
{
return $table
->columns([
TextColumn::make('title'),
// Only visible to admins
TextColumn::make('internal_notes')
->visible(fn (): bool => auth()->user()->isAdmin()),
// Toggleable but hidden by default for non-admins
TextColumn::make('cost')
->money('USD')
->toggleable(
isToggledHiddenByDefault: !auth()->user()->isAdmin()
),
]);
}Multi-Tenancy
Tenant Setup
<?php
namespace App\Providers\Filament;
use App\Models\Team;
use Filament\Panel;
use Filament\PanelProvider;
class AdminPanelProvider extends PanelProvider
{
public function panel(Panel $panel): Panel
{
return $panel
// ...
->tenant(Team::class)
->tenantRegistration(\App\Filament\Pages\Tenancy\RegisterTeam::class)
->tenantProfile(\App\Filament\Pages\Tenancy\EditTeamProfile::class)
->tenantMenuItems([
\Filament\Actions\Action::make('settings')
->url(fn (): string => TeamSettings::getUrl())
->icon('heroicon-m-cog-8-tooth'),
]);
}
}User Model with Tenancy
<?php
namespace App\Models;
use Filament\Models\Contracts\FilamentUser;
use Filament\Models\Contracts\HasDefaultTenant;
use Filament\Models\Contracts\HasTenants;
use Filament\Panel;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsToMany;
use Illuminate\Foundation\Auth\User as Authenticatable;
use Illuminate\Support\Collection;
class User extends Authenticatable implements FilamentUser, HasTenants, HasDefaultTenant
{
public function teams(): BelongsToMany
{
return $this->belongsToMany(Team::class)
->withTimestamps()
->withPivot('role');
}
public function getTenants(Panel $panel): Collection
{
return $this->teams;
}
public function canAccessTenant(Model $tenant): bool
{
return $this->teams()
->whereKey($tenant->getKey())
->exists();
}
public function getDefaultTenant(Panel $panel): ?Model
{
return $this->latestTeam;
}
public function latestTeam()
{
return $this->belongsTo(Team::class, 'latest_team_id');
}
}Tenant Registration Page
<?php
namespace App\Filament\Pages\Tenancy;
use App\Models\Team;
use Filament\Forms\Components\TextInput;
use Filament\Forms\Form;
use Filament\Pages\Tenancy\RegisterTenant;
use Filament\Schemas\Schema;
class RegisterTeam extends RegisterTenant
{
public static function getLabel(): string
{
return 'Register team';
}
public function form(Schema $schema): Schema
{
return $schema
->components([
TextInput::make('name')
->required()
->maxLength(255),
TextInput::make('slug')
->required()
->unique('teams', 'slug')
->maxLength(255),
]);
}
protected function handleRegistration(array $data): Team
{
$team = Team::create($data);
$team->members()->attach(auth()->user(), ['role' => 'owner']);
return $team;
}
}Tenant Profile Page
<?php
namespace App\Filament\Pages\Tenancy;
use Filament\Forms\Components\TextInput;
use Filament\Forms\Form;
use Filament\Pages\Tenancy\EditTenantProfile;
use Filament\Schemas\Schema;
class EditTeamProfile extends EditTenantProfile
{
public static function getLabel(): string
{
return 'Team profile';
}
public function form(Schema $schema): Schema
{
return $schema
->components([
TextInput::make('name')
->required()
->maxLength(255),
TextInput::make('slug')
->required()
->unique('teams', 'slug', ignoreRecord: true)
->maxLength(255),
]);
}
}Tenant Subdomain Routing
public function panel(Panel $panel): Panel
{
return $panel
// ...
->tenant(Team::class, slugAttribute: 'slug')
->tenantDomain('{tenant:slug}.example.com');
}Tenant Path Routing
public function panel(Panel $panel): Panel
{
return $panel
// ...
->tenant(Team::class, slugAttribute: 'slug')
->tenantRoutePrefix('{tenant:slug}');
}Tenant-Aware Resources
class PostResource extends Resource
{
public static function getEloquentQuery(): Builder
{
return parent::getEloquentQuery()
->whereBelongsTo(Filament::getTenant());
}
}Or use a global scope:
class Post extends Model
{
protected static function booted(): void
{
static::addGlobalScope('tenant', function (Builder $query) {
if (auth()->hasUser() && Filament::getTenant()) {
$query->whereBelongsTo(Filament::getTenant());
}
});
static::creating(function (Post $post) {
if (Filament::getTenant()) {
$post->team()->associate(Filament::getTenant());
}
});
}
}Role-Based Access Control
Using Spatie Laravel-Permission
composer require spatie/laravel-permission
php artisan vendor:publish --provider="Spatie\Permission\PermissionServiceProvider"User Model
<?php
namespace App\Models;
use Filament\Models\Contracts\FilamentUser;
use Filament\Panel;
use Illuminate\Foundation\Auth\User as Authenticatable;
use Spatie\Permission\Traits\HasRoles;
class User extends Authenticatable implements FilamentUser
{
use HasRoles;
public function canAccessPanel(Panel $panel): bool
{
return $this->hasAnyRole(['admin', 'editor', 'author']);
}
public function isAdmin(): bool
{
return $this->hasRole('admin');
}
public function isEditor(): bool
{
return $this->hasRole('editor');
}
}Resource with Role Checks
class PostResource extends Resource
{
public static function canCreate(): bool
{
return auth()->user()->hasAnyRole(['admin', 'editor', 'author']);
}
public static function canEdit(Model $record): bool
{
$user = auth()->user();
if ($user->hasRole('admin')) {
return true;
}
if ($user->hasRole('editor')) {
return true;
}
return $user->id === $record->user_id;
}
public static function canDelete(Model $record): bool
{
return auth()->user()->hasRole('admin');
}
}Permission-Based Checks
public static function canPublish(): bool
{
return auth()->user()->can('publish posts');
}
public static function canFeature(): bool
{
return auth()->user()->can('feature posts');
}Navigation Visibility
Resource Navigation
class PostResource extends Resource
{
// Hide from navigation entirely
protected static bool $shouldRegisterNavigation = false;
// Dynamic visibility
public static function shouldRegisterNavigation(): bool
{
return auth()->user()->can('view posts');
}
}Page Navigation
class Reports extends Page
{
public static function shouldRegisterNavigation(): bool
{
return auth()->user()->can('view reports');
}
}Advanced Authorization
Custom Authorization Logic
class PostResource extends Resource
{
public static function canEdit(Model $record): bool
{
$user = auth()->user();
// Admin can edit anything
if ($user->isAdmin()) {
return true;
}
// Owner can edit their own posts
if ($user->id === $record->user_id) {
// But not if locked
if ($record->isLocked()) {
return false;
}
// And not if published (only drafts editable by owner)
if ($record->status === 'published') {
return false;
}
return true;
}
// Editors can edit any draft
if ($user->isEditor() && $record->status === 'draft') {
return true;
}
return false;
}
}Time-Based Restrictions
public static function canEdit(Model $record): bool
{
// Can't edit posts older than 30 days (unless admin)
if ($record->created_at->diffInDays(now()) > 30 && !auth()->user()->isAdmin()) {
return false;
}
return auth()->user()->can('update', $record);
}Feature Flags
public static function canCreate(): bool
{
// Check if feature is enabled
if (!Feature::active('new-posts')) {
return false;
}
return auth()->user()->can('create posts');
}Best Practices
1. Use policies for model-level authorization 2. Implement FilamentUser for panel access 3. Check permissions in actions, not just visibility 4. Use custom policy methods for specific actions 5. Implement multi-tenancy at the query level 6. Cache expensive permission checks 7. Test authorization thoroughly 8. Use roles for broad access, permissions for specific actions 9. Keep authorization logic in policies, not controllers 10. Use Response::deny() with messages for clarity 11. Document your authorization rules 12. Regularly audit permissions 13. Use middleware for route-level protection 14. Implement proper tenant isolation 15. Log sensitive authorization failures
Testing Authorization
use function Pest\Laravel\actingAs;
it('denies access to guests', function () {
auth()->logout();
livewire(ListPosts::class)
->assertRedirect('/login');
});
it('denies access to unauthorized users', function () {
$user = User::factory()->create(['role' => 'user']);
actingAs($user);
livewire(ListPosts::class)
->assertForbidden();
});
it('allows access to authorized users', function () {
$admin = User::factory()->create(['role' => 'admin']);
actingAs($admin);
livewire(ListPosts::class)
->assertOk();
});
it('hides actions without permission', function () {
$user = User::factory()->create();
$user->revokePermissionTo('delete posts');
actingAs($user);
$post = Post::factory()->create();
livewire(ListPosts::class)
->assertTableActionHidden('delete', $post);
});Additional Resources
Filament v5 Code Examples
Complete working code examples for Filament v5 components.
Panel Provider
Basic Configuration
<?php
namespace App\Providers\Filament;
use Filament\Panel;
use Filament\PanelProvider;
use Filament\Support\Colors\Color;
class AdminPanelProvider extends PanelProvider
{
public function panel(Panel $panel): Panel
{
return $panel
->id('admin')
->path('admin')
->colors(['primary' => Color::Amber])
->discoverResources(in: app_path('Filament/Resources'), for: 'App\Filament\Resources')
->discoverPages(in: app_path('Filament/Pages'), for: 'App\Filament\Pages')
->discoverWidgets(in: app_path('Filament/Widgets'), for: 'App\Filament\Widgets')
->middleware([
\Illuminate\Cookie\Middleware\EncryptCookies::class,
\Illuminate\Session\Middleware\StartSession::class,
])
->authMiddleware([
\Illuminate\Auth\Middleware\Authenticate::class,
]);
}
}With Branding
public function panel(Panel $panel): Panel
{
return $panel
->id('admin')
->path('admin')
->brandName('My Admin')
->brandLogo(asset('images/logo.svg'))
->favicon(asset('images/favicon.ico'))
->colors([
'primary' => '#f59e0b',
'secondary' => '#64748b',
'success' => '#22c55e',
'warning' => '#f59e0b',
'danger' => '#ef4444',
]);
}Resources
Complete Post Resource
<?php
namespace App\Filament\Resources;
use App\Filament\Resources\PostResource\Pages;
use App\Models\Post;
use Filament\Forms;
use Filament\Forms\Form;
use Filament\Resources\Resource;
use Filament\Tables;
use Filament\Tables\Table;
class PostResource extends Resource
{
protected static ?string $model = Post::class;
protected static ?string $navigationIcon = 'heroicon-o-document-text';
protected static ?string $navigationGroup = 'Content';
public static function form(Form $form): Form
{
return $form
->schema([
Forms\Components\TextInput::make('title')
->required()
->maxLength(255)
->live(onBlur: true)
->afterStateUpdated(fn ($state, $set) =>
$set('slug', \Illuminate\Support\Str::slug($state))
),
Forms\Components\TextInput::make('slug')
->required()
->unique(ignoreRecord: true)
->maxLength(255),
Forms\Components\RichEditor::make('content')
->required()
->columnSpanFull(),
Forms\Components\Select::make('status')
->options([
'draft' => 'Draft',
'published' => 'Published',
'archived' => 'Archived',
])
->required(),
Forms\Components\DateTimePicker::make('published_at'),
Forms\Components\Toggle::make('is_featured'),
Forms\Components\Select::make('author_id')
->relationship('author', 'name')
->searchable()
->preload()
->required(),
Forms\Components\Select::make('categories')
->relationship('categories', 'name')
->multiple()
->preload()
->searchable(),
])
->columns(2);
}
public static function table(Table $table): Table
{
return $table
->columns([
Tables\Columns\TextColumn::make('title')
->searchable()
->sortable(),
Tables\Columns\TextColumn::make('status')
->badge()
->color(fn (string $state): string => match ($state) {
'draft' => 'gray',
'published' => 'success',
'archived' => 'warning',
}),
Tables\Columns\TextColumn::make('author.name')
->searchable()
->sortable(),
Tables\Columns\IconColumn::make('is_featured')
->boolean(),
Tables\Columns\TextColumn::make('created_at')
->dateTime()
->sortable(),
])
->filters([
Tables\Filters\SelectFilter::make('status')
->options([
'draft' => 'Draft',
'published' => 'Published',
'archived' => 'Archived',
]),
Tables\Filters\TernaryFilter::make('is_featured'),
])
->actions([
Tables\Actions\ViewAction::make(),
Tables\Actions\EditAction::make(),
Tables\Actions\DeleteAction::make(),
])
->bulkActions([
Tables\Actions\BulkActionGroup::make([
Tables\Actions\DeleteBulkAction::make(),
]),
]);
}
public static function getPages(): array
{
return [
'index' => Pages\ListPosts::class,
'create' => Pages\CreatePost::class,
'view' => Pages\ViewPost::class,
'edit' => Pages\EditPost::class,
];
}
}Forms
User Registration Form
use Filament\Forms\Components\TextInput;
use Filament\Forms\Components\Select;
use Filament\Forms\Components\DatePicker;
use Filament\Forms\Components\FileUpload;
use Filament\Forms\Components\Toggle;
use Filament\Forms\Components\Section;
use Filament\Forms\Components\Grid;
public static function form(Form $form): Form
{
return $form
->schema([
Section::make('Personal Information')
->schema([
TextInput::make('first_name')
->required()
->maxLength(255),
TextInput::make('last_name')
->required()
->maxLength(255),
TextInput::make('email')
->email()
->required()
->unique(ignoreRecord: true),
DatePicker::make('birthdate')
->maxDate(now()->subYears(18)),
])
->columns(2),
Section::make('Account Settings')
->schema([
TextInput::make('password')
->password()
->required()
->minLength(8)
->confirmed(),
TextInput::make('password_confirmation')
->password()
->required(),
Select::make('role')
->options([
'admin' => 'Administrator',
'editor' => 'Editor',
'user' => 'User',
])
->required(),
Toggle::make('is_active')
->default(true),
])
->columns(2),
Section::make('Profile')
->schema([
FileUpload::make('avatar')
->image()
->circleCropper()
->maxSize(5120),
]),
]);
}Product Form with Repeater
use Filament\Forms\Components\Repeater;
use Filament\Forms\Components\TextInput;
use Filament\Forms\Components\Select;
public static function form(Form $form): Form
{
return $form
->schema([
TextInput::make('name')
->required(),
Repeater::make('variants')
->schema([
Select::make('size')
->options(['S' => 'Small', 'M' => 'Medium', 'L' => 'Large'])
->required(),
TextInput::make('sku')
->required(),
TextInput::make('price')
->numeric()
->prefix('$')
->required(),
TextInput::make('stock')
->numeric()
->required(),
])
->columns(4)
->defaultItems(1)
->addActionLabel('Add Variant')
->reorderable(),
]);
}Settings Form with Tabs
use Filament\Forms\Components\Tabs;
use Filament\Forms\Components\TextInput;
use Filament\Forms\Components\Textarea;
public static function form(Form $form): Form
{
return $form
->schema([
Tabs::make('Settings')
->tabs([
Tabs\Tab::make('General')
->schema([
TextInput::make('site_name')
->required(),
TextInput::make('site_email')
->email()
->required(),
]),
Tabs\Tab::make('SEO')
->schema([
TextInput::make('meta_title'),
Textarea::make('meta_description')
->maxLength(160),
]),
Tabs\Tab::make('Social')
->schema([
TextInput::make('facebook_url'),
TextInput::make('twitter_url'),
]),
]),
]);
}Tables
Product Catalog Table
public static function table(Table $table): Table
{
return $table
->columns([
Tables\Columns\ImageColumn::make('image')
->square()
->size(50),
Tables\Columns\TextColumn::make('name')
->searchable()
->sortable(),
Tables\Columns\TextColumn::make('category.name')
->searchable()
->sortable(),
Tables\Columns\TextColumn::make('price')
->money('USD')
->sortable(),
Tables\Columns\TextColumn::make('stock_quantity')
->numeric()
->sortable()
->color(fn (int $state): string => match (true) {
$state <= 0 => 'danger',
$state <= 10 => 'warning',
default => 'success',
}),
Tables\Columns\IconColumn::make('is_active')
->boolean(),
])
->filters([
Tables\Filters\SelectFilter::make('category')
->relationship('category', 'name'),
Tables\Filters\Filter::make('low_stock')
->query(fn ($query) => $query->where('stock_quantity', '<=', 10))
->toggle(),
])
->actions([
Tables\Actions\EditAction::make(),
Tables\Actions\DeleteAction::make(),
])
->bulkActions([
Tables\Actions\BulkActionGroup::make([
Tables\Actions\DeleteBulkAction::make(),
]),
]);
}Actions
Email Action with Modal
use Filament\Actions\Action;
use Filament\Notifications\Notification;
Action::make('sendEmail')
->icon('heroicon-m-envelope')
->form([
TextInput::make('subject')
->required()
->maxLength(255),
RichEditor::make('body')
->required()
->columnSpanFull(),
])
->action(function (array $data, $record): void {
Mail::to($record->email)
->send(new GenericEmail($data['subject'], $data['body']));
Notification::make()
->title('Email sent successfully')
->success()
->send();
})
->modalWidth(MaxWidth::Large);Delete with Confirmation
Action::make('delete')
->color('danger')
->icon('heroicon-m-trash')
->requiresConfirmation()
->modalHeading('Delete record')
->modalDescription('Are you sure? This cannot be undone.')
->action(fn ($record) => $record->delete());Widgets
Stats Overview
<?php
namespace App\Filament\Widgets;
use App\Models\Order;
use App\Models\User;
use Filament\Widgets\StatsOverviewWidget;
use Filament\Widgets\StatsOverviewWidget\Stat;
class DashboardStats extends StatsOverviewWidget
{
protected function getStats(): array
{
return [
Stat::make('Total Users', User::count())
->description(User::where('created_at', '>=', now()->subDays(30))->count() . ' new this month')
->color('success'),
Stat::make('Revenue', '$' . number_format(Order::sum('total'), 2))
->description('Total sales')
->color('primary'),
Stat::make('Pending Orders', Order::where('status', 'pending')->count())
->color('warning'),
];
}
}Chart Widget
<?php
namespace App\Filament\Widgets;
use App\Models\Order;
use Filament\Widgets\ChartWidget;
class OrdersChart extends ChartWidget
{
protected ?string $heading = 'Orders per Month';
protected function getData(): array
{
$orders = Order::query()
->selectRaw('MONTH(created_at) as month, COUNT(*) as count')
->whereYear('created_at', now()->year)
->groupBy('month')
->pluck('count', 'month')
->toArray();
return [
'datasets' => [
[
'label' => 'Orders',
'data' => array_values($orders),
],
],
'labels' => ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun',
'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'],
];
}
protected function getType(): string
{
return 'line';
}
}Testing
Resource Tests
<?php
use App\Filament\Resources\PostResource\Pages\CreatePost;
use App\Filament\Resources\PostResource\Pages\EditPost;
use App\Filament\Resources\PostResource\Pages\ListPosts;
use App\Models\Post;
use App\Models\User;
use function Pest\Laravel\actingAs;
use function Pest\Livewire\livewire;
beforeEach(function () {
$this->user = User::factory()->create();
actingAs($this->user);
});
// List Page
it('can list posts', function () {
$posts = Post::factory()->count(5)->create();
livewire(ListPosts::class)
->assertCanSeeTableRecords($posts);
});
it('can search posts', function () {
Post::factory()->create(['title' => 'Hello World']);
livewire(ListPosts::class)
->searchTable('Hello')
->assertCanSeeTableRecords(Post::where('title', 'like', '%Hello%')->get());
});
// Create Page
it('can create a post', function () {
$newData = Post::factory()->make();
livewire(CreatePost::class)
->fillForm([
'title' => $newData->title,
'content' => $newData->content,
])
->call('create')
->assertNotified()
->assertRedirect();
});
it('validates required fields', function () {
livewire(CreatePost::class)
->fillForm(['title' => null])
->call('create')
->assertHasFormErrors(['title']);
});
// Edit Page
it('can update a post', function () {
$post = Post::factory()->create();
$newData = Post::factory()->make();
livewire(EditPost::class, ['record' => $post->id])
->fillForm(['title' => $newData->title])
->call('save')
->assertNotified();
});Authorization
User Model with Panel Access
<?php
namespace App\Models;
use Filament\Models\Contracts\FilamentUser;
use Filament\Panel;
use Illuminate\Foundation\Auth\User as Authenticatable;
class User extends Authenticatable implements FilamentUser
{
public function canAccessPanel(Panel $panel): bool
{
return match ($panel->getId()) {
'admin' => $this->isAdmin(),
'app' => $this->hasVerifiedEmail(),
default => false,
};
}
}Policy
<?php
namespace App\Policies;
use App\Models\Post;
use App\Models\User;
class PostPolicy
{
public function viewAny(User $user): bool
{
return true;
}
public function view(User $user, Post $post): bool
{
return $user->id === $post->user_id || $user->isAdmin();
}
public function create(User $user): bool
{
return $user->can('create posts');
}
public function update(User $user, Post $post): bool
{
return $user->id === $post->user_id || $user->isAdmin();
}
public function delete(User $user, Post $post): bool
{
return $user->isAdmin();
}
}Resource with Authorization
class PostResource extends Resource
{
public static function canCreate(): bool
{
return auth()->user()->can('create posts');
}
public static function canEdit($record): bool
{
return auth()->user()->can('update', $record);
}
public static function form(Form $form): Form
{
return $form
->schema([
TextInput::make('title')->required(),
// Only visible to admins
TextInput::make('internal_notes')
->visible(fn (): bool => auth()->user()->isAdmin()),
]);
}
}Multi-Tenancy
User with Tenancy
<?php
namespace App\Models;
use Filament\Models\Contracts\HasTenants;
use Filament\Panel;
use Illuminate\Database\Eloquent\Relations\BelongsToMany;
use Illuminate\Foundation\Auth\User as Authenticatable;
use Illuminate\Support\Collection;
class User extends Authenticatable implements HasTenants
{
public function teams(): BelongsToMany
{
return $this->belongsToMany(Team::class);
}
public function getTenants(Panel $panel): Collection
{
return $this->teams;
}
public function canAccessTenant(Model $tenant): bool
{
return $this->teams()->whereKey($tenant)->exists();
}
}Panel with Tenancy
public function panel(Panel $panel): Panel
{
return $panel
->tenant(Team::class)
->tenantProfile(\App\Filament\Pages\TeamProfile::class)
->tenantRegistration(\App\Filament\Pages\TeamRegistration::class);
}Custom Pages
Settings Page
<?php
namespace App\Filament\Pages;
use Filament\Actions\Action;
use Filament\Forms\Components\TextInput;
use Filament\Forms\Form;
use Filament\Notifications\Notification;
use Filament\Pages\Page;
class Settings extends Page
{
protected static ?string $navigationIcon = 'heroicon-o-cog';
protected static ?string $navigationGroup = 'System';
protected static string $view = 'filament.pages.settings';
public ?array $data = [];
public function mount(): void
{
$this->form->fill([
'site_name' => config('app.name'),
]);
}
public static function canAccess(): bool
{
return auth()->user()->isAdmin();
}
public function form(Form $form): Form
{
return $form
->schema([
TextInput::make('site_name')->required(),
])
->statePath('data');
}
protected function getHeaderActions(): array
{
return [
Action::make('save')
->action('save'),
];
}
public function save(): void
{
$data = $this->form->getState();
setting(['site_name' => $data['site_name']]);
Notification::make()
->title('Settings saved')
->success()
->send();
}
}Relation Managers
Comments Relation Manager
<?php
namespace App\Filament\Resources\PostResource\RelationManagers;
use Filament\Forms;
use Filament\Forms\Form;
use Filament\Resources\RelationManagers\RelationManager;
use Filament\Tables;
use Filament\Tables\Table;
class CommentsRelationManager extends RelationManager
{
protected static string $relationship = 'comments';
public function form(Form $form): Form
{
return $form
->schema([
Forms\Components\Textarea::make('content')
->required()
->columnSpanFull(),
]);
}
public function table(Table $table): Table
{
return $table
->columns([
Tables\Columns\TextColumn::make('content')->limit(50),
Tables\Columns\TextColumn::make('user.name'),
])
->headerActions([
Tables\Actions\CreateAction::make(),
])
->actions([
Tables\Actions\EditAction::make(),
Tables\Actions\DeleteAction::make(),
]);
}
}Complete Blog Example
Category Resource
<?php
namespace App\Filament\Resources;
use App\Models\Category;
use Filament\Forms;
use Filament\Forms\Form;
use Filament\Resources\Resource;
use Filament\Tables;
use Filament\Tables\Table;
use Illuminate\Support\Str;
class CategoryResource extends Resource
{
protected static ?string $model = Category::class;
protected static ?string $navigationIcon = 'heroicon-o-tag';
protected static ?string $navigationGroup = 'Blog';
public static function form(Form $form): Form
{
return $form
->schema([
Forms\Components\TextInput::make('name')
->required()
->live(onBlur: true)
->afterStateUpdated(fn ($state, $set) =>
$set('slug', Str::slug($state))
),
Forms\Components\TextInput::make('slug')
->required()
->unique(ignoreRecord: true),
Forms\Components\ColorPicker::make('color'),
]);
}
public static function table(Table $table): Table
{
return $table
->columns([
Tables\Columns\ColorColumn::make('color'),
Tables\Columns\TextColumn::make('name')->searchable(),
Tables\Columns\TextColumn::make('posts_count')->counts('posts'),
]);
}
}Post Resource with Relations
<?php
namespace App\Filament\Resources;
use App\Models\Post;
use Filament\Forms;
use Filament\Forms\Form;
use Filament\Resources\Resource;
use Filament\Tables;
use Filament\Tables\Table;
class PostResource extends Resource
{
protected static ?string $model = Post::class;
protected static ?string $navigationIcon = 'heroicon-o-document-text';
protected static ?string $navigationGroup = 'Blog';
public static function form(Form $form): Form
{
return $form
->schema([
Forms\Components\TextInput::make('title')->required(),
Forms\Components\RichEditor::make('content')
->required()
->columnSpanFull(),
Forms\Components\Select::make('category_id')
->relationship('category', 'name')
->searchable(),
Forms\Components\Select::make('tags')
->relationship('tags', 'name')
->multiple()
->preload(),
Forms\Components\Toggle::make('is_published'),
])
->columns(2);
}
public static function table(Table $table): Table
{
return $table
->columns([
Tables\Columns\TextColumn::make('title')->searchable(),
Tables\Columns\TextColumn::make('category.name'),
Tables\Columns\IconColumn::make('is_published')->boolean(),
]);
}
public static function getRelations(): array
{
return [
RelationManagers\CommentsRelationManager::class,
];
}
}Form Components Reference
Complete reference for Filament v5 form components and schemas.
Available Form Fields
Filament v5 provides 20+ form field types in the Filament\Forms\Components namespace:
| Field Type | Class | Description |
|---|---|---|
| Text Input | TextInput | Single-line text input with validation |
| Textarea | Textarea | Multi-line text input |
| Select | Select | Dropdown selection |
| Checkbox | Checkbox | Boolean checkbox |
| Toggle | Toggle | Switch toggle |
| Checkbox List | CheckboxList | Multiple checkboxes |
| Radio | Radio | Radio button group |
| Date Picker | DatePicker | Date selection |
| DateTime Picker | DateTimePicker | Date and time selection |
| File Upload | FileUpload | File upload with preview |
| Rich Editor | RichEditor | WYSIWYG editor (TipTap) |
| Markdown Editor | MarkdownEditor | Markdown editing |
| Repeater | Repeater | Repeatable field groups |
| Builder | Builder | Block-based content builder |
| Tags Input | TagsInput | Tag creation |
| Key-value | KeyValue | Key-value pairs |
| Color Picker | ColorPicker | Color selection |
| Toggle Buttons | ToggleButtons | Button group selection |
| Slider | Slider | Range slider |
| Hidden | Hidden | Hidden input field |
Text Input
use Filament\Forms\Components\TextInput;
TextInput::make('name')
->required()
->maxLength(255)
->minLength(2)
->autocomplete()
->autofocus()
->placeholder('Enter name')
->prefix('Mr/Ms')
->suffix('@company.com')
->helperText('Your full name')
->hint('Required')
->hintIcon('heroicon-m-question-mark-circle')
->disabled()
->readonly()
->hidden()
->dehydrated(false) // Don't save to database
->live() // Real-time updates
->afterStateUpdated(fn ($state, $set) => $set('slug', Str::slug($state)))Validation Methods
TextInput::make('email')
->email()
->required()
->unique('users', 'email', ignoreRecord: true)
->maxLength(255)
TextInput::make('password')
->password()
->required()
->minLength(8)
->regex('/^(?=.*[A-Z])(?=.*\d).+$/')
->confirmed() // Requires password_confirmation field
TextInput::make('slug')
->required()
->alphaDash()
->unique('posts', 'slug', ignoreRecord: true)Select
use Filament\Forms\Components\Select;
Select::make('status')
->options([
'draft' => 'Draft',
'published' => 'Published',
'archived' => 'Archived',
])
->required()
->searchable()
->preload()
->multiple()
->native(false)
->placeholder('Select a status')
->noSearchResultsMessage('No status found')
->loadingMessage('Loading statuses...')
->searchPrompt('Search for a status')Relationship Selection
Select::make('author_id')
->relationship('author', 'name')
->searchable()
->preload()
->createOptionForm([
TextInput::make('name')->required(),
TextInput::make('email')->email()->required(),
])
->editOptionForm([
TextInput::make('name')->required(),
])
->createOptionAction(fn ($data, $set) => $set('author_id', $data['id']))Dynamic Options
Select::make('city')
->options(fn (Get $get): array => match ($get('country')) {
'usa' => ['nyc' => 'New York', 'la' => 'Los Angeles'],
'uk' => ['london' => 'London', 'manchester' => 'Manchester'],
default => [],
})
->live()
->afterStateUpdated(fn (Set $set) => $set('zip', null))Checkbox & Toggle
use Filament\Forms\Components\Checkbox;
use Filament\Forms\Components\Toggle;
Checkbox::make('is_active')
->label('Active')
->helperText('Check to activate')
->inline()
Toggle::make('is_featured')
->label('Featured')
->onColor('success')
->offColor('danger')
->inline(false)Date Pickers
use Filament\Forms\Components\DatePicker;
use Filament\Forms\Components\DateTimePicker;
use Filament\Forms\Components\TimePicker;
DatePicker::make('birthdate')
->required()
->minDate(now()->subYears(100))
->maxDate(now()->subYears(18))
->native(false)
->displayFormat('M d, Y')
DateTimePicker::make('published_at')
->required()
->seconds(false)
->timezone('America/New_York')
TimePicker::make('opening_time')
->required()
->withoutSeconds()File Upload
use Filament\Forms\Components\FileUpload;
use Filament\Forms\Components\ImageUpload;
FileUpload::make('attachment')
->required()
->multiple()
->directory('attachments')
->disk('s3')
->preserveFilenames()
->maxSize(10240) // 10MB
->minSize(1024) // 1MB
->acceptedFileTypes(['application/pdf', 'image/*'])
->maxFiles(5)
->openable()
->downloadable()
->previewable()
->imagePreviewHeight('250')
ImageUpload::make('avatar')
->image()
->imageEditor()
->imageEditorAspectRatios([
null,
'16:9',
'4:3',
'1:1',
])
->circleCropper()
->squareCropper()Rich Editor
use Filament\Forms\Components\RichEditor;
RichEditor::make('content')
->required()
->columnSpanFull()
->toolbarButtons([
'attachFiles',
'blockquote',
'bold',
'bulletList',
'codeBlock',
'h2',
'h3',
'italic',
'link',
'orderedList',
'redo',
'strike',
'underline',
'undo',
])
->fileAttachmentsDirectory('posts/images')
->fileAttachmentsDisk('s3')Repeater
use Filament\Forms\Components\Repeater;
Repeater::make('items')
->schema([
TextInput::make('name')->required(),
TextInput::make('quantity')->numeric()->required(),
TextInput::make('price')->numeric()->prefix('$')->required(),
])
->columns(3)
->defaultItems(1)
->addActionLabel('Add Item')
->reorderable(true)
->collapsible()
->collapsed()
->cloneable()
->grid(2)
->maxItems(10)
->minItems(1)
->deleteAction(
fn (Action $action) => $action->requiresConfirmation(),
)Builder (Block Editor)
use Filament\Forms\Components\Builder;
Builder::make('content')
->blocks([
Builder\Block::make('heading')
->schema([
TextInput::make('content')
->label('Heading')
->required(),
Select::make('level')
->options([
'h1' => 'Heading 1',
'h2' => 'Heading 2',
'h3' => 'Heading 3',
])
->required(),
])
->label('Heading'),
Builder\Block::make('paragraph')
->schema([
RichEditor::make('content')
->label('Paragraph')
->required(),
])
->label('Paragraph'),
Builder\Block::make('image')
->schema([
FileUpload::make('image')
->image()
->required(),
TextInput::make('caption'),
])
->label('Image'),
])
->collapsible()
->defaultItems(0)Layout Components
Grid
use Filament\Forms\Components\Grid;
Grid::make(2)
->schema([
TextInput::make('first_name'),
TextInput::make('last_name'),
])
// Responsive grid
Grid::make([
'default' => 1,
'sm' => 2,
'lg' => 3,
'xl' => 4,
])->schema([
// ...
])Section
use Filament\Forms\Components\Section;
Section::make('Personal Information')
->description('Enter your personal details')
->icon('heroicon-m-user')
->collapsible()
->collapsed()
->compact()
->aside() // Side-by-side layout
->schema([
TextInput::make('name'),
TextInput::make('email'),
])
->columns(2)Tabs
use Filament\Forms\Components\Tabs;
Tabs::make('Settings')
->tabs([
Tabs\Tab::make('General')
->icon('heroicon-m-cog')
->schema([
TextInput::make('site_name'),
TextInput::make('site_email'),
]),
Tabs\Tab::make('SEO')
->icon('heroicon-m-globe')
->schema([
TextInput::make('meta_title'),
Textarea::make('meta_description'),
]),
Tabs\Tab::make('Social')
->icon('heroicon-m-share')
->schema([
TextInput::make('facebook_url'),
TextInput::make('twitter_url'),
]),
])Wizard
use Filament\Forms\Components\Wizard;
Wizard::make([
Wizard\Step::make('Account')
->icon('heroicon-m-user')
->description('Create your account')
->schema([
TextInput::make('email')
->email()
->required(),
TextInput::make('password')
->password()
->required()
->confirmed(),
]),
Wizard\Step::make('Profile')
->icon('heroicon-m-identification')
->description('Set up your profile')
->schema([
TextInput::make('name')->required(),
FileUpload::make('avatar')->image(),
]),
Wizard\Step::make('Preferences')
->icon('heroicon-m-cog')
->description('Customize your experience')
->schema([
Toggle::make('newsletter'),
Select::make('timezone'),
]),
])
->skippable()
->persistInQueryString()
->submitAction(
Action::make('createAccount')
->label('Create Account')
)Fieldset
use Filament\Forms\Components\Fieldset;
Fieldset::make('Address')
->schema([
TextInput::make('street'),
TextInput::make('city'),
TextInput::make('zip'),
])Groups
use Filament\Forms\Components\Group;
Group::make()
->schema([
// Fields that should be grouped together
])
->columnSpanFull()
->columns(2)Placeholder
use Filament\Forms\Components\Placeholder;
Placeholder::make('summary')
->content(fn ($record): string => "Created: {$record->created_at}")
->columnSpanFull()KeyValue
use Filament\Forms\Components\KeyValue;
KeyValue::make('meta')
->keyLabel('Property')
->valueLabel('Value')
->addActionLabel('Add Property')
->keyPlaceholder('Property name')
->valuePlaceholder('Property value')
->reorderable()Tags Input
use Filament\Forms\Components\TagsInput;
TagsInput::make('skills')
->label('Skills')
->placeholder('Add a skill')
->suggestions(['PHP', 'Laravel', 'JavaScript', 'Vue.js', 'React'])
->splitKeys(['Tab', ',', 'Enter'])
->reorderable()Color Picker
use Filament\Forms\Components\ColorPicker;
ColorPicker::make('color')
->label('Brand Color')
->hex()
->rgb()
->rgba()
->hsl()
->hsv()
->default('#f59e0b')Toggle Buttons
use Filament\Forms\Components\ToggleButtons;
ToggleButtons::make('size')
->options([
'sm' => 'Small',
'md' => 'Medium',
'lg' => 'Large',
])
->icons([
'sm' => 'heroicon-m-sun',
'md' => 'heroicon-m-moon',
'lg' => 'heroicon-m-star',
])
->default('md')
->inline()
->grouped()Slider
use Filament\Forms\Components\Slider;
Slider::make('volume')
->min(0)
->max(100)
->step(10)
->default(50)
->suffix('%')Validation
Built-in Rules
TextInput::make('email')
->required()
->email()
->unique('users', 'email', ignoreRecord: true)
->maxLength(255)
->minLength(5)
TextInput::make('password')
->required()
->minLength(8)
->regex('/^(?=.*[A-Z])(?=.*\d).+$/')
->confirmed() // Requires password_confirmation
TextInput::make('age')
->numeric()
->minValue(18)
->maxValue(100)
->integer()
TextInput::make('website')
->url()
->activeUrl()
TextInput::make('ip')
->ip()
TextInput::make('mac')
->macAddress()Custom Rules
TextInput::make('username')
->rules(['required', 'string', 'min:3', 'max:20', 'regex:/^[a-zA-Z0-9_]+$/'])
TextInput::make('code')
->rule(function ($state) {
return $state === 'valid-code' ? null : 'Invalid code';
})Conditional Rules
TextInput::make('company_name')
->required(fn (Get $get): bool => $get('is_company'))
TextInput::make('phone')
->requiredWithout('email')Conditional Visibility
TextInput::make('company_name')
->visible(fn (Get $get): bool => $get('is_company'))
->hidden(fn (Get $get): bool => ! $get('is_company'))
->disabled(fn (Get $get): bool => $get('is_locked'))
->readonly(fn (): bool => auth()->user()->cannot('edit'))State Management
use Filament\Forms\Components\Utilities\Get;
use Filament\Forms\Components\Utilities\Set;
Select::make('country')
->live()
->afterStateUpdated(fn (Set $set) => $set('city', null))
TextInput::make('name')
->live(onBlur: true)
->afterStateUpdated(fn ($state, Set $set) => $set('slug', Str::slug($state)))
// Access other field values
TextInput::make('total')
->formatStateUsing(fn ($state, Get $get): string =>
$get('quantity') * $get('price')
)
->dehydrated(false)Complete Example: Product Form
public static function form(Form $form): Form
{
return $form
->schema([
Section::make('Basic Information')
->schema([
TextInput::make('name')
->required()
->maxLength(255)
->live(onBlur: true)
->afterStateUpdated(fn ($state, $set) =>
$set('slug', Str::slug($state))
),
TextInput::make('slug')
->required()
->unique(ignoreRecord: true)
->disabled()
->dehydrated(),
Select::make('category_id')
->relationship('category', 'name')
->searchable()
->preload()
->required(),
Textarea::make('description')
->required()
->maxLength(1000)
->columnSpanFull(),
])
->columns(2),
Section::make('Pricing & Inventory')
->schema([
TextInput::make('price')
->required()
->numeric()
->prefix('$')
->maxValue(999999.99),
TextInput::make('sale_price')
->numeric()
->prefix('$')
->lte('price', 'Must be less than or equal to regular price'),
TextInput::make('stock_quantity')
->required()
->numeric()
->integer()
->minValue(0),
Toggle::make('track_inventory')
->default(true),
])
->columns(2),
Section::make('Media')
->schema([
FileUpload::make('images')
->multiple()
->image()
->maxFiles(10)
->directory('products')
->columnSpanFull(),
]),
Section::make('SEO')
->schema([
TextInput::make('meta_title')
->maxLength(70),
Textarea::make('meta_description')
->maxLength(160)
->columnSpanFull(),
])
->collapsible()
->collapsed(),
]);
}Tips & Best Practices
1. Use sections to group related fields 2. Leverage live() for real-time updates 3. Always validate input with built-in methods 4. Use afterStateUpdated to compute derived fields 5. Disable fields that shouldn't be edited instead of hiding them 6. Use placeholders for computed/display-only values 7. Enable reorderable() on repeaters for better UX 8. Use builder blocks for flexible content structures 9. Add helper text and hints for complex fields 10. Test forms with various input combinations
Additional Resources
Infolists Reference
Complete guide for creating read-only data displays in Filament v5.
Overview
Infolists are components for rendering "description lists" - read-only displays of data in a label-value format. They are commonly used on view pages, custom pages, and relation managers to present record information.
Basic Infolist
Simple Infolist
<?php
namespace App\Filament\Resources\PostResource;
use Filament\Infolists\Components\TextEntry;
use Filament\Infolists\Infolist;
use Filament\Resources\Resource;
class PostResource extends Resource
{
public static function infolist(Infolist $infolist): Infolist
{
return $infolist
->schema([
TextEntry::make('title'),
TextEntry::make('content')
->columnSpanFull(),
TextEntry::make('created_at')
->dateTime(),
]);
}
}Entry Types
Text Entry
use Filament\Infolists\Components\TextEntry;
TextEntry::make('name')
->label('Full Name')
->weight('bold') // 'bold', 'semibold', 'medium'
->color('primary') // 'primary', 'success', 'danger', etc.
->icon('heroicon-m-user')
->iconColor('primary')
->copyable()
->copyMessage('Copied!')
->copyMessageDuration(1500)
->limit(50)
->tooltip('Full name of the user')
->placeholder('No name provided')
->prefix('Mr/Ms ')
->suffix(' (verified)')
->alignLeft() // alignLeft, alignCenter, alignRight
->columnSpanFull()
->hidden(fn ($record) => !$record->name)
->visible(fn ($record) => $record->isPublished());Text Formatting
TextEntry::make('price')
->money('USD') // Currency formatting
->formatStateUsing(fn ($state) => number_format($state, 2));
TextEntry::make('created_at')
->dateTime('M j, Y g:i A') // Custom date format
->since(); // Relative time ("2 hours ago")
TextEntry::make('content')
->markdown() // Render as markdown
->html() // Render as HTML
->prose() // Apply prose styling
->columnSpanFull();
TextEntry::make('slug')
->url(fn ($record) => route('posts.show', $record))
->openUrlInNewTab();
TextEntry::make('status')
->badge()
->color(fn (string $state): string => match ($state) {
'draft' => 'gray',
'published' => 'success',
'archived' => 'danger',
});Icon Entry
use Filament\Infolists\Components\IconEntry;
IconEntry::make('is_active')
->boolean() // Shows check/x icons
->trueIcon('heroicon-m-check-circle')
->falseIcon('heroicon-m-x-circle')
->trueColor('success')
->falseColor('danger');
IconEntry::make('status')
->icon(fn (string $state): string => match ($state) {
'active' => 'heroicon-m-check-circle',
'inactive' => 'heroicon-m-x-circle',
default => 'heroicon-m-question-mark-circle',
})
->color(fn (string $state): string => match ($state) {
'active' => 'success',
'inactive' => 'danger',
default => 'gray',
});Image Entry
use Filament\Infolists\Components\ImageEntry;
ImageEntry::make('avatar')
->disk('public')
->square() // Square aspect ratio
->circular() // Circular (avatar style)
->size(100) // Size in pixels
->width(200)
->height(200)
->checkFileExistence()
->defaultImageUrl(fn ($record) => 'https://ui-avatars.com/api/?name=' . urlencode($record->name));
ImageEntry::make('gallery')
->multiple()
->limit(3) // Show only 3 images
->limitedRemainingText(); // Show "+X more" textColor Entry
use Filament\Infolists\Components\ColorEntry;
ColorEntry::make('brand_color')
->copyable();Key-Value Entry
use Filament\Infolists\Components\KeyValueEntry;
KeyValueEntry::make('meta')
->keyLabel('Property')
->valueLabel('Value');Repeatable Entry
use Filament\Infolists\Components\RepeatableEntry;
use Filament\Infolists\Components\TextEntry;
RepeatableEntry::make('orderItems')
->schema([
TextEntry::make('product.name'),
TextEntry::make('quantity'),
TextEntry::make('price')
->money('USD'),
])
->columns(3)
->contained(false); // Remove card stylingLayout Components
Section
use Filament\Infolists\Components\Section;
Section::make('Personal Information')
->description('User details and contact information')
->icon('heroicon-m-user')
->collapsible()
->collapsed()
->compact()
->aside() // Side-by-side label/value
->schema([
TextEntry::make('name'),
TextEntry::make('email'),
TextEntry::make('phone'),
])
->columns(2);Grid
use Filament\Infolists\Components\Grid;
Grid::make(2)
->schema([
TextEntry::make('first_name'),
TextEntry::make('last_name'),
]);
// Responsive grid
Grid::make([
'default' => 1,
'sm' => 2,
'lg' => 3,
])
->schema([
// ...
]);Tabs
use Filament\Infolists\Components\Tabs;
Tabs::make('User Details')
->tabs([
Tabs\Tab::make('General')
->icon('heroicon-m-user')
->schema([
TextEntry::make('name'),
TextEntry::make('email'),
]),
Tabs\Tab::make('Professional')
->icon('heroicon-m-briefcase')
->schema([
TextEntry::make('job_title'),
TextEntry::make('company'),
]),
Tabs\Tab::make('Social')
->icon('heroicon-m-share')
->schema([
TextEntry::make('twitter'),
TextEntry::make('linkedin'),
]),
]);Split
use Filament\Infolists\Components\Split;
Split::make([
Section::make('Details')
->schema([
TextEntry::make('name'),
TextEntry::make('email'),
]),
Section::make('Avatar')
->schema([
ImageEntry::make('avatar')
->circular()
->size(150),
]),
])
->from('lg'); // Split on large screensFieldset
use Filament\Infolists\Components\Fieldset;
Fieldset::make('Address')
->schema([
TextEntry::make('street'),
TextEntry::make('city'),
TextEntry::make('zip'),
]);Complete Examples
User Profile Infolist
public static function infolist(Infolist $infolist): Infolist
{
return $infolist
->schema([
Section::make('Profile')
->schema([
Split::make([
Grid::make(2)
->schema([
TextEntry::make('name')
->weight('bold')
->size(TextEntry\Size::Large),
TextEntry::make('email')
->icon('heroicon-m-envelope')
->copyable(),
TextEntry::make('phone')
->icon('heroicon-m-phone'),
TextEntry::make('role.name')
->badge()
->color('primary'),
IconEntry::make('is_active')
->boolean()
->label('Status'),
]),
ImageEntry::make('avatar')
->circular()
->size(150)
->hiddenLabel(),
])
->from('md'),
]),
Section::make('Professional Information')
->collapsible()
->schema([
TextEntry::make('job_title'),
TextEntry::make('company'),
TextEntry::make('bio')
->markdown()
->columnSpanFull(),
])
->columns(2),
Section::make('Metadata')
->collapsed()
->schema([
TextEntry::make('created_at')
->dateTime()
->since(),
TextEntry::make('updated_at')
->dateTime()
->since(),
])
->columns(2),
]);
}Order Details Infolist
public static function infolist(Infolist $infolist): Infolist
{
return $infolist
->schema([
Section::make('Order Summary')
->schema([
TextEntry::make('order_number')
->label('Order #')
->weight('bold'),
TextEntry::make('status')
->badge()
->color(fn (string $state): string => match ($state) {
'pending' => 'warning',
'processing' => 'info',
'completed' => 'success',
'cancelled' => 'danger',
}),
TextEntry::make('total')
->money('USD')
->weight('bold')
->size(TextEntry\Size::Large),
TextEntry::make('created_at')
->dateTime(),
])
->columns(4),
Section::make('Customer')
->schema([
TextEntry::make('customer.name')
->label('Name'),
TextEntry::make('customer.email')
->label('Email')
->copyable(),
TextEntry::make('customer.phone')
->label('Phone'),
])
->columns(3),
Section::make('Order Items')
->schema([
RepeatableEntry::make('items')
->hiddenLabel()
->schema([
TextEntry::make('product.name')
->label('Product'),
TextEntry::make('quantity')
->label('Qty'),
TextEntry::make('price')
->label('Price')
->money('USD'),
TextEntry::make('total')
->label('Total')
->money('USD')
->weight('bold'),
])
->columns(4)
->contained(false),
]),
]);
}Advanced Features
Entry Groups
use Filament\Infolists\Components\EntryGroup;
Section::make('Contact Information')
->schema([
EntryGroup::make([
TextEntry::make('email')
->icon('heroicon-m-envelope'),
TextEntry::make('phone')
->icon('heroicon-m-phone'),
TextEntry::make('website')
->icon('heroicon-m-globe')
->url()
->openUrlInNewTab(),
])
->label('Contact Details')
->inlineLabel(),
]);Custom Entries
use Filament\Infolists\Components\Entry;
Entry::make('custom')
->label('Custom Data')
->view('filament.infolists.entries.custom')
->viewData([
'extra' => 'data',
]);Relationship Data
TextEntry::make('author.name')
->label('Author')
->url(fn ($record) => UserResource::getUrl('view', ['record' => $record->author]))
->openUrlInNewTab();
TextEntry::make('tags.name')
->badge()
->separator(',');Conditional Visibility
Section::make('Premium Features')
->visible(fn ($record) => $record->isPremium())
->schema([
TextEntry::make('premium_feature_1'),
TextEntry::make('premium_feature_2'),
]);
TextEntry::make('admin_notes')
->visible(fn () => auth()->user()->isAdmin());State Formatting
TextEntry::make('price')
->formatStateUsing(fn ($state) => '$' . number_format($state, 2));
TextEntry::make('tags')
->formatStateUsing(function ($state) {
return collect($state)->pluck('name')->join(', ');
});
TextEntry::make('status')
->formatStateUsing(fn ($state) => ucfirst($state));Using Infolists in Resources
Resource Infolist
class PostResource extends Resource
{
public static function infolist(Infolist $infolist): Infolist
{
return $infolist
->schema([
// ... entries
]);
}
public static function getPages(): array
{
return [
'index' => Pages\ListPosts::class,
'create' => Pages\CreatePost::class,
'view' => Pages\ViewPost::class, // Uses infolist
'edit' => Pages\EditPost::class,
];
}
}Custom Page Infolist
<?php
namespace App\Filament\Pages;
use Filament\Infolists\Components\TextEntry;
use Filament\Infolists\Infolist;
use Filament\Pages\Page;
class Dashboard extends Page
{
protected static string $view = 'filament.pages.dashboard';
public ?array $data = [];
public function mount(): void
{
$this->data = [
'total_users' => User::count(),
'total_orders' => Order::count(),
];
}
public function infolist(Infolist $infolist): Infolist
{
return $infolist
->state($this->data)
->schema([
TextEntry::make('total_users')
->label('Total Users'),
TextEntry::make('total_orders')
->label('Total Orders'),
]);
}
}Infolist in Actions
Action::make('viewDetails')
->infolist([
TextEntry::make('name'),
TextEntry::make('email'),
])
->record($user)
->modalWidth(MaxWidth::Large);Best Practices
1. Use sections to group related data - Organize logically 2. Leverage icons - Enhance visual recognition 3. Format dates consistently - Use since() or custom formats 4. Copyable fields - Enable for emails, IDs, URLs 5. Use badges for statuses - Visual state indicators 6. Collapsible sections - For less important data 7. Responsive layouts - Use responsive grids 8. Hide empty fields - Use hidden() or placeholder() 9. Link related resources - Connect to other pages 10. Keep it scannable - Use bold labels, clear hierarchy
Additional Resources
Notifications Reference
Complete guide for sending notifications in Filament v5.
Overview
Filament provides a powerful notification system for sending flash messages, database notifications, and broadcast notifications. Notifications can include actions, custom styling, and can be sent to specific users.
Basic Notifications
Simple Notifications
use Filament\Notifications\Notification;
// Success notification
Notification::make()
->title('Saved successfully')
->success()
->send();
// Error notification
Notification::make()
->title('Error occurred')
->body('Unable to save the record.')
->danger()
->send();
// Warning notification
Notification::make()
->title('Warning')
->body('This action cannot be undone.')
->warning()
->send();
// Info notification
Notification::make()
->title('Information')
->body('New updates are available.')
->info()
->send();Notification Types
| Method | Color | Use Case |
|---|---|---|
success() | Green | Successful operations |
danger() | Red | Errors and failures |
warning() | Yellow | Warnings and cautions |
info() | Blue | Informational messages |
secondary() | Gray | Neutral messages |
Notification Properties
Notification::make()
->title('Title here') // Main heading
->body('Description here') // Detailed message
->icon('heroicon-o-check') // Custom icon
->iconColor('success') // Icon color
->duration(5000) // Auto-dismiss after ms (default: 5000)
->persistent() // Don't auto-dismiss
->send();Notifications with Actions
Basic Actions
use Filament\Actions\Action;
Notification::make()
->title('Order placed successfully')
->success()
->body('Your order #12345 has been confirmed.')
->actions([
Action::make('view')
->button()
->url(route('orders.show', $order))
->openUrlInNewTab(),
Action::make('undo')
->color('gray')
->close()
->action(function () {
// Undo logic
}),
])
->send();Action Types
Notification::make()
->actions([
// Button style action
Action::make('view')
->button()
->url('/orders/123'),
// Link style action
Action::make('dismiss')
->link()
->close(),
// Action with dispatch
Action::make('refresh')
->dispatch('refreshData'),
// Action with color
Action::make('delete')
->color('danger')
->requiresConfirmation()
->action(fn () => $record->delete()),
])
->send();Database Notifications
Sending to Database
Database notifications are stored and displayed in the notification center:
// Send to specific user
Notification::make()
->title('New comment on your post')
->body('John Doe commented on "My First Post"')
->actions([
Action::make('view')
->url(route('posts.show', $post)),
])
->sendToDatabase($user);
// Send to multiple users
$users = User::where('role', 'admin')->get();
Notification::make()
->title('System maintenance scheduled')
->body('Maintenance will occur tonight at 2 AM.')
->sendToDatabase($users);Marking as Read
// User marks notification as read
$user->notifications()->find($notificationId)->markAsRead();
// Mark all as read
$user->unreadNotifications->markAsRead();Broadcast Notifications
Broadcasting to Users
Requires Laravel Echo and a broadcast driver (Pusher, Ably, etc.):
// Send broadcast notification
Notification::make()
->title('New order received')
->body('Order #12345 needs processing.')
->broadcast($user);Using in Laravel Notifications
use App\Models\User;
use Filament\Notifications\Notification;
use Illuminate\Notifications\Messages\BroadcastMessage;
class OrderReceived extends Notification
{
public function toBroadcast(User $notifiable): BroadcastMessage
{
return Notification::make()
->title('New order received')
->body('Order #12345 needs processing.')
->getBroadcastMessage();
}
public function via($notifiable): array
{
return ['broadcast', 'database'];
}
}Notification in Actions
After Action Completion
use Filament\Actions\Action;
use Filament\Notifications\Notification;
Action::make('publish')
->action(function (Post $record) {
$record->update(['status' => 'published']);
Notification::make()
->title('Post published')
->success()
->send();
})
->successNotification(
Notification::make()
->title('Published successfully')
->success()
);Custom Notification in Actions
Action::make('process')
->action(function () {
try {
$this->processData();
Notification::make()
->title('Processing complete')
->success()
->send();
} catch (Exception $e) {
Notification::make()
->title('Processing failed')
->body($e->getMessage())
->danger()
->persistent()
->send();
}
});Notification Center
Enabling Notification Center
Add to your PanelProvider:
use Filament\Notifications\Livewire\DatabaseNotifications;
public function panel(Panel $panel): Panel
{
return $panel
// ...
->databaseNotifications()
->databaseNotificationsPolling('30s');
}Customizing Notification Center
// Change polling interval
->databaseNotificationsPolling('60s')
// Disable polling
->databaseNotificationsPolling(null)Custom Notification Views
Blade Component
Create resources/views/vendor/filament/components/notification.blade.php:
<x-filament::notification
:notification="$notification"
class="custom-notification"
>
{{ $slot }}
</x-filament::notification>Custom Styling
/* Add to your CSS */
.filament-notification {
@apply rounded-lg shadow-lg;
}
.filament-notification.success {
@apply border-l-4 border-green-500;
}Advanced Examples
Complex Notification
Notification::make()
->title('Export completed')
->icon('heroicon-o-document-arrow-down')
->iconColor('success')
->body('Your data export is ready for download.')
->actions([
Action::make('download')
->button()
->color('primary')
->icon('heroicon-o-arrow-down-tray')
->url(fn () => Storage::url('exports/data.csv'))
->openUrlInNewTab(),
Action::make('dismiss')
->link()
->close()
->label('Dismiss'),
])
->persistent()
->send();Notification with Loading State
Action::make('generateReport')
->action(function () {
Notification::make()
->title('Generating report...')
->body('This may take a few minutes.')
->persistent()
->send();
// Long running task
$report = $this->generateReport();
Notification::make()
->title('Report generated')
->success()
->actions([
Action::make('download')
->url($report->downloadUrl()),
])
->send();
});Conditional Notifications
public function save()
{
$result = $this->form->save();
if ($result->successful) {
Notification::make()
->title('Saved successfully')
->success()
->send();
} else {
Notification::make()
->title('Save failed')
->body($result->errorMessage)
->danger()
->persistent()
->actions([
Action::make('retry')
->button()
->action(fn () => $this->save()),
])
->send();
}
}Testing Notifications
use function Pest\Livewire\livewire;
it('shows success notification after create', function () {
livewire(CreatePost::class)
->fillForm(['title' => 'Test'])
->call('create')
->assertNotified('Post created successfully');
});
it('shows custom notification', function () {
livewire(CreatePost::class)
->callAction('publish')
->assertNotified(
Notification::make()
->title('Published!')
->success()
);
});Best Practices
1. Keep messages concise - Title should be brief, use body for details 2. Use appropriate types - Success for completions, danger for errors 3. Add actions when helpful - Link to affected resources 4. Use persistent for important - Don't auto-dismiss critical messages 5. Send database notifications - For actions requiring later attention 6. Test notification flow - Verify users receive proper feedback 7. Don't over-notify - Too many notifications reduce effectiveness 8. Use icons appropriately - Enhance recognition with relevant icons 9. Consider mobile - Ensure notifications work well on small screens 10. Localize messages - Use translation keys for multi-language support
Additional Resources
Schemas Reference
Complete guide for understanding and using Filament v5's schema system.
Overview
Schemas are the foundation of Filament's Server-Driven UI approach. They allow you to build user interfaces declaratively using PHP configuration objects rather than writing HTML or JavaScript. Schemas define the structure and behavior of forms, infolists, tables, and layouts.
What Are Schemas?
A schema is a collection of components that define:
- Form fields and their validation rules
- Infolist entries for displaying data
- Table columns and their formatting
- Layout containers (grids, sections, tabs)
- Action definitions and their behavior
Schema Types
Form Schemas
Used in resources, custom pages, and actions:
use Filament\Forms\Components\TextInput;
use Filament\Forms\Components\Select;
use Filament\Forms\Components\Section;
use Filament\Forms\Form;
public function form(Form $form): Form
{
return $form
->schema([
Section::make('Details')
->schema([
TextInput::make('name')
->required()
->maxLength(255),
Select::make('status')
->options(['draft' => 'Draft', 'published' => 'Published'])
->required(),
])
->columns(2),
]);
}Infolist Schemas
Used for read-only data display:
use Filament\Infolists\Components\TextEntry;
use Filament\Infolists\Components\Section;
use Filament\Infolists\Infolist;
public function infolist(Infolist $infolist): Infolist
{
return $infolist
->schema([
Section::make('Details')
->schema([
TextEntry::make('name'),
TextEntry::make('status')
->badge(),
])
->columns(2),
]);
}Table Schemas
Define table columns and filters:
use Filament\Tables\Columns\TextColumn;
use Filament\Tables\Columns\BadgeColumn;
use Filament\Tables\Table;
public function table(Table $table): Table
{
return $table
->columns([
TextColumn::make('name')
->searchable(),
BadgeColumn::make('status')
->color(fn ($state) => match ($state) {
'draft' => 'gray',
'published' => 'success',
}),
])
->filters([
// Filters
]);
}Layout Components
Grid
Organize components in columns:
use Filament\Schemas\Components\Grid;
Grid::make(2)
->schema([
TextInput::make('first_name'),
TextInput::make('last_name'),
])
// Responsive grid
Grid::make([
'default' => 1,
'sm' => 2,
'md' => 3,
'lg' => 4,
])
->schema([
// Components
])Section
Group components with a heading:
use Filament\Schemas\Components\Section;
Section::make('Personal Information')
->description('Enter your personal details')
->icon('heroicon-m-user')
->collapsible()
->collapsed()
->compact()
->aside() // Side-by-side layout
->schema([
TextInput::make('name'),
TextInput::make('email'),
])
->columns(2);Tabs
Organize into tabs:
use Filament\Schemas\Components\Tabs;
Tabs::make('Settings')
->tabs([
Tabs\Tab::make('General')
->icon('heroicon-m-cog')
->schema([
TextInput::make('site_name'),
]),
Tabs\Tab::make('SEO')
->icon('heroicon-m-globe')
->schema([
TextInput::make('meta_title'),
]),
]);Wizard
Multi-step form:
use Filament\Schemas\Components\Wizard;
Wizard::make([
Wizard\Step::make('Account')
->icon('heroicon-m-user')
->description('Create your account')
->schema([
TextInput::make('email'),
TextInput::make('password'),
]),
Wizard\Step::make('Profile')
->icon('heroicon-m-identification')
->description('Set up your profile')
->schema([
TextInput::make('name'),
]),
])
->skippable()
->persistInQueryString();Fieldset
Group without card styling:
use Filament\Schemas\Components\Fieldset;
Fieldset::make('Address')
->schema([
TextInput::make('street'),
TextInput::make('city'),
]);Split
Side-by-side layout:
use Filament\Schemas\Components\Split;
Split::make([
Section::make('Details')
->schema([
TextInput::make('name'),
]),
Section::make('Avatar')
->schema([
ImageUpload::make('avatar'),
]),
])
->from('lg'); // BreakpointGroup
Simple grouping:
use Filament\Schemas\Components\Group;
Group::make()
->schema([
// Components
])
->columnSpanFull()
->columns(2);Component States
State Management
use Filament\Schemas\Components\Utilities\Get;
use Filament\Schemas\Components\Utilities\Set;
TextInput::make('title')
->live(onBlur: true)
->afterStateUpdated(fn ($state, Set $set) =>
$set('slug', Str::slug($state))
);
// Accessing other field values
TextInput::make('total')
->formatStateUsing(fn ($state, Get $get) =>
$get('quantity') * $get('price')
)
->dehydrated(false);State Hydration
TextInput::make('name')
->formatStateUsing(fn ($state) => strtoupper($state))
->dehydrateStateUsing(fn ($state) => strtolower($state));Default Values
TextInput::make('status')
->default('draft');
Select::make('role')
->default('user');
Toggle::make('is_active')
->default(true);Conditional Logic
Visible/Hidden
TextInput::make('company_name')
->visible(fn (Get $get) => $get('is_company'))
->hidden(fn (Get $get) => !$get('is_company'));Disabled/Readonly
TextInput::make('email')
->disabled(fn () => auth()->user()->cannot('edit_email'));
TextInput::make('created_at')
->readonly();Required
TextInput::make('company_name')
->required(fn (Get $get) => $get('is_company'));Schema Validation
Field-Level Validation
TextInput::make('email')
->email()
->required()
->unique('users', 'email')
->maxLength(255);
TextInput::make('password')
->password()
->required()
->minLength(8)
->confirmed();Custom Rules
TextInput::make('username')
->rules(['required', 'string', 'regex:/^[a-z0-9_]+$/']);
TextInput::make('code')
->rule(function ($state) {
return $state === 'VALID' ? null : 'Invalid code';
});Validation Messages
TextInput::make('email')
->email()
->validationMessages([
'email' => 'Please enter a valid email address.',
'required' => 'Email is required.',
]);Schema Customization
Columns
Section::make('Details')
->schema([
TextInput::make('name'),
TextInput::make('email'),
TextInput::make('phone'),
])
->columns(2);
// Responsive columns
Section::make('Details')
->columns([
'default' => 1,
'sm' => 2,
'lg' => 3,
]);Column Span
TextInput::make('title')
->columnSpan(2);
TextInput::make('content')
->columnSpanFull();
// Responsive span
TextInput::make('name')
->columnSpan([
'default' => 1,
'lg' => 2,
]);Extra Attributes
TextInput::make('name')
->extraAttributes(['class' => 'custom-class'])
->extraInputAttributes(['autocomplete' => 'off']);Advanced Patterns
Dynamic Schema
public function form(Form $form): Form
{
return $form
->schema(fn (): array => [
TextInput::make('name'),
// Conditionally include fields
...(auth()->user()->isAdmin() ? [
TextInput::make('admin_notes'),
] : []),
]);
}Builder Pattern
use Filament\Forms\Components\Builder;
Builder::make('content')
->blocks([
Builder\Block::make('heading')
->schema([
TextInput::make('content'),
Select::make('level'),
]),
Builder\Block::make('paragraph')
->schema([
RichEditor::make('content'),
]),
]);Repeater Pattern
use Filament\Forms\Components\Repeater;
Repeater::make('items')
->schema([
TextInput::make('name'),
TextInput::make('quantity'),
])
->collapsible()
->itemLabel(fn (array $state): ?string => $state['name'] ?? null);Schema Composition
// Reusable schema class
class UserSchema
{
public static function make(): array
{
return [
TextInput::make('name')->required(),
TextInput::make('email')->email()->required(),
];
}
}
// Use in resource
public function form(Form $form): Form
{
return $form
->schema([
...UserSchema::make(),
TextInput::make('phone'),
]);
}Schema in Different Contexts
In Resources
class PostResource extends Resource
{
public static function form(Form $form): Form
{
return $form
->schema([
// Form schema
]);
}
public static function infolist(Infolist $infolist): Infolist
{
return $infolist
->schema([
// Infolist schema
]);
}
public static function table(Table $table): Table
{
return $table
->columns([
// Table schema (columns)
]);
}
}In Custom Pages
class Settings extends Page
{
protected ?array $data = [];
public function form(Form $form): Form
{
return $form
->statePath('data')
->schema([
TextInput::make('site_name'),
]);
}
}In Actions
Action::make('edit')
->form([
TextInput::make('name'),
TextInput::make('email'),
])
->action(function (array $data) {
// Handle data
});Schema State Path
State Path Configuration
public function form(Form $form): Form
{
return $form
->statePath('data') // Root state path
->schema([
TextInput::make('name'), // Accesses $data['name']
Section::make('Address')
->statePath('address') // Nested path
->schema([
TextInput::make('street'), // $data['address']['street']
]),
]);
}Dehydration Control
TextInput::make('display_value')
->dehydrated(false) // Don't save to database
->formatStateUsing(fn ($state, Get $get) =>
$get('quantity') * $get('price')
);Schema Testing
Testing Forms
it('validates form schema', function () {
livewire(CreatePost::class)
->fillForm([
'title' => null,
])
->call('create')
->assertHasFormErrors(['title' => 'required']);
});
it('fills form schema correctly', function () {
livewire(EditPost::class, ['record' => $post])
->assertFormSet([
'title' => $post->title,
]);
});Testing Conditional Logic
it('shows company fields when is_company is true', function () {
livewire(CreateUser::class)
->fillForm(['is_company' => true])
->assertFormFieldVisible('company_name');
});Best Practices
1. Keep schemas organized - Use sections and clear naming 2. Extract reusable schemas - Create schema classes for common patterns 3. Use live sparingly - Too many live fields hurt performance 4. Leverage conditional logic - Show/hide based on context 5. Validate at field level - Use built-in validation methods 6. Test schema behavior - Verify conditional logic works correctly 7. Use responsive layouts - Grid columns that adapt to screen size 8. Minimize nesting - Avoid deeply nested structures 9. Document complex schemas - Add comments for clarity 10. Reuse components - Create custom components for repetition
Common Patterns
Address Schema
public static function addressSchema(): array
{
return [
Grid::make(2)
->schema([
TextInput::make('street')
->required()
->columnSpanFull(),
TextInput::make('city')
->required(),
TextInput::make('state')
->required(),
TextInput::make('zip')
->required()
->maxLength(10),
TextInput::make('country')
->required(),
]),
];
}Contact Schema
public static function contactSchema(): array
{
return [
TextInput::make('email')
->email()
->required(),
TextInput::make('phone')
->tel(),
TextInput::make('website')
->url()
->prefix('https://'),
];
}Additional Resources
Related skills
How it compares
Choose filament-pro over generic Laravel skills when the task specifically involves Filament v5 admin scaffolding rather than general routing or Eloquent patterns.
FAQ
What Laravel version does filament-pro require?
filament-pro requires Laravel 11.28 or newer, PHP 8.2+, Livewire v4, and TailwindCSS v4.1+. The skill targets Filament v5's Schemas API and server-driven admin components built on that stack.
What does filament-pro generate for admin panels?
filament-pro guides agents to produce Filament v5 CRUD resources, form schemas, data tables, dashboard widgets, and admin interface tests. Output uses declarative PHP without custom JavaScript for Livewire v4 reactivity.
Is Filament Pro safe to install?
skills.sh reports 2 of 3 security scanners passed. Review the Security Audits panel on this page before installing in production.