@elyracode/stack-filament 0.4.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,1841 @@
1
+ ---
2
+ name: filament
3
+ description: Deep knowledge of Filament v5 for Laravel. Use when building admin panels, CRUD resources, data tables, forms, actions, widgets, dashboards, multi-tenancy, or any Filament feature in a Laravel application.
4
+ ---
5
+
6
+ # Filament v5 Reference
7
+
8
+ ## Installation
9
+
10
+ ```bash
11
+ composer require filament/filament
12
+ php artisan filament:install --panels
13
+ ```
14
+
15
+ Create the first admin user:
16
+
17
+ ```bash
18
+ php artisan make:filament-user
19
+ ```
20
+
21
+ ## Artisan Commands
22
+
23
+ ```bash
24
+ php artisan make:filament-resource Customer
25
+ php artisan make:filament-resource Customer --generate # Auto-generate form/table from DB columns
26
+ php artisan make:filament-resource Customer --simple # Modal CRUD (no separate create/edit pages)
27
+ php artisan make:filament-resource Customer --soft-deletes
28
+ php artisan make:filament-resource Customer --view # Include a view page
29
+ php artisan make:filament-page Settings
30
+ php artisan make:filament-widget SalesChart --chart
31
+ php artisan make:filament-widget LatestOrders --table
32
+ php artisan make:filament-relation-manager CategoryResource posts title
33
+ php artisan make:filament-cluster Settings
34
+ ```
35
+
36
+ ## Panel Configuration
37
+
38
+ ```php
39
+ // app/Providers/Filament/AdminPanelProvider.php
40
+ use Filament\Panel;
41
+ use Filament\PanelProvider;
42
+ use Filament\Support\Colors\Color;
43
+
44
+ class AdminPanelProvider extends PanelProvider
45
+ {
46
+ public function panel(Panel $panel): Panel
47
+ {
48
+ return $panel
49
+ ->default()
50
+ ->id('admin')
51
+ ->path('admin')
52
+ ->login()
53
+ ->registration()
54
+ ->passwordReset()
55
+ ->emailVerification()
56
+ ->colors([
57
+ 'primary' => Color::Indigo,
58
+ 'danger' => Color::Rose,
59
+ 'gray' => Color::Zinc,
60
+ 'success' => Color::Emerald,
61
+ 'warning' => Color::Amber,
62
+ ])
63
+ ->font('Inter')
64
+ ->brandName('My App')
65
+ ->brandLogo(asset('images/logo.svg'))
66
+ ->favicon(asset('images/favicon.png'))
67
+ ->sidebarCollapsibleOnDesktop()
68
+ ->maxContentWidth('full')
69
+ ->discoverResources(in: app_path('Filament/Resources'), for: 'App\\Filament\\Resources')
70
+ ->discoverPages(in: app_path('Filament/Pages'), for: 'App\\Filament\\Pages')
71
+ ->discoverWidgets(in: app_path('Filament/Widgets'), for: 'App\\Filament\\Widgets')
72
+ ->discoverClusters(in: app_path('Filament/Clusters'), for: 'App\\Filament\\Clusters')
73
+ ->middleware([...])
74
+ ->authMiddleware([...])
75
+ ->databaseNotifications()
76
+ ->spa();
77
+ }
78
+ }
79
+ ```
80
+
81
+ ## Resources (CRUD)
82
+
83
+ ### v5 File Structure
84
+
85
+ ```
86
+ app/Filament/Resources/Customers/
87
+ CustomerResource.php # Main resource class
88
+ Pages/
89
+ CreateCustomer.php
90
+ EditCustomer.php
91
+ ListCustomers.php
92
+ ViewCustomer.php # Optional
93
+ Schemas/
94
+ CustomerForm.php # Form schema (split from resource)
95
+ Tables/
96
+ CustomersTable.php # Table definition (split from resource)
97
+ ```
98
+
99
+ ### Resource Class
100
+
101
+ ```php
102
+ <?php
103
+
104
+ namespace App\Filament\Resources\Customers;
105
+
106
+ use App\Models\Customer;
107
+ use Filament\Resources\Resource;
108
+ use Filament\Schemas\Schema;
109
+ use Filament\Tables\Table;
110
+
111
+ class CustomerResource extends Resource
112
+ {
113
+ protected static ?string $model = Customer::class;
114
+ protected static ?string $navigationIcon = 'heroicon-o-user-group';
115
+ protected static ?string $navigationGroup = 'Shop';
116
+ protected static ?int $navigationSort = 2;
117
+ protected static ?string $recordTitleAttribute = 'name';
118
+ protected static ?string $slug = 'customers';
119
+
120
+ public static function form(Schema $schema): Schema
121
+ {
122
+ return CustomerForm::configure($schema);
123
+ }
124
+
125
+ public static function table(Table $table): Table
126
+ {
127
+ return CustomersTable::configure($table);
128
+ }
129
+
130
+ public static function getRelations(): array
131
+ {
132
+ return [
133
+ OrdersRelationManager::class,
134
+ ];
135
+ }
136
+
137
+ public static function getPages(): array
138
+ {
139
+ return [
140
+ 'index' => ListCustomers::route('/'),
141
+ 'create' => CreateCustomer::route('/create'),
142
+ 'edit' => EditCustomer::route('/{record}/edit'),
143
+ ];
144
+ }
145
+
146
+ public static function getGloballySearchableAttributes(): array
147
+ {
148
+ return ['name', 'email', 'phone'];
149
+ }
150
+
151
+ public static function getGlobalSearchResultDetails(\Illuminate\Database\Eloquent\Model $record): array
152
+ {
153
+ return [
154
+ 'Email' => $record->email,
155
+ ];
156
+ }
157
+ }
158
+ ```
159
+
160
+ ### Form Schema Class (v5 split pattern)
161
+
162
+ ```php
163
+ <?php
164
+
165
+ namespace App\Filament\Resources\Customers\Schemas;
166
+
167
+ use Filament\Forms\Components\TextInput;
168
+ use Filament\Forms\Components\Select;
169
+ use Filament\Forms\Components\DatePicker;
170
+ use Filament\Forms\Components\Toggle;
171
+ use Filament\Schemas\Components\Section;
172
+ use Filament\Schemas\Components\Grid;
173
+ use Filament\Schemas\Schema;
174
+
175
+ class CustomerForm
176
+ {
177
+ public static function configure(Schema $schema): Schema
178
+ {
179
+ return $schema->components([
180
+ Section::make('Personal Information')
181
+ ->schema([
182
+ Grid::make(2)->schema([
183
+ TextInput::make('name')
184
+ ->required()
185
+ ->maxLength(255),
186
+ TextInput::make('email')
187
+ ->email()
188
+ ->required()
189
+ ->unique(ignoreRecord: true),
190
+ ]),
191
+ TextInput::make('phone')
192
+ ->tel(),
193
+ DatePicker::make('date_of_birth')
194
+ ->native(false)
195
+ ->maxDate(now()),
196
+ ]),
197
+ Section::make('Settings')
198
+ ->schema([
199
+ Select::make('status')
200
+ ->options([
201
+ 'active' => 'Active',
202
+ 'inactive' => 'Inactive',
203
+ 'blocked' => 'Blocked',
204
+ ])
205
+ ->default('active')
206
+ ->required(),
207
+ Toggle::make('is_vip')
208
+ ->label('VIP Customer'),
209
+ ])
210
+ ->collapsible(),
211
+ ]);
212
+ }
213
+ }
214
+ ```
215
+
216
+ ### Table Class (v5 split pattern)
217
+
218
+ ```php
219
+ <?php
220
+
221
+ namespace App\Filament\Resources\Customers\Tables;
222
+
223
+ use Filament\Tables\Table;
224
+ use Filament\Tables\Columns\TextColumn;
225
+ use Filament\Tables\Columns\IconColumn;
226
+ use Filament\Tables\Filters\SelectFilter;
227
+ use Filament\Actions\EditAction;
228
+ use Filament\Actions\DeleteBulkAction;
229
+ use Filament\Actions\BulkActionGroup;
230
+
231
+ class CustomersTable
232
+ {
233
+ public static function configure(Table $table): Table
234
+ {
235
+ return $table
236
+ ->columns([
237
+ TextColumn::make('name')
238
+ ->searchable()
239
+ ->sortable(),
240
+ TextColumn::make('email')
241
+ ->searchable()
242
+ ->copyable(),
243
+ TextColumn::make('status')
244
+ ->badge()
245
+ ->color(fn (string $state): string => match ($state) {
246
+ 'active' => 'success',
247
+ 'inactive' => 'gray',
248
+ 'blocked' => 'danger',
249
+ }),
250
+ IconColumn::make('is_vip')
251
+ ->boolean(),
252
+ TextColumn::make('created_at')
253
+ ->dateTime()
254
+ ->sortable()
255
+ ->toggleable(isToggledHiddenByDefault: true),
256
+ ])
257
+ ->filters([
258
+ SelectFilter::make('status')
259
+ ->options([
260
+ 'active' => 'Active',
261
+ 'inactive' => 'Inactive',
262
+ 'blocked' => 'Blocked',
263
+ ]),
264
+ ])
265
+ ->recordActions([
266
+ EditAction::make(),
267
+ ])
268
+ ->toolbarActions([
269
+ BulkActionGroup::make([
270
+ DeleteBulkAction::make(),
271
+ ]),
272
+ ]);
273
+ }
274
+ }
275
+ ```
276
+
277
+ ### Page Classes
278
+
279
+ ```php
280
+ // Pages/ListCustomers.php
281
+ <?php
282
+
283
+ namespace App\Filament\Resources\Customers\Pages;
284
+
285
+ use App\Filament\Resources\Customers\CustomerResource;
286
+ use Filament\Resources\Pages\ListRecords;
287
+ use Filament\Actions\CreateAction;
288
+
289
+ class ListCustomers extends ListRecords
290
+ {
291
+ protected static string $resource = CustomerResource::class;
292
+
293
+ protected function getHeaderActions(): array
294
+ {
295
+ return [CreateAction::make()];
296
+ }
297
+ }
298
+
299
+ // Pages/CreateCustomer.php
300
+ <?php
301
+
302
+ namespace App\Filament\Resources\Customers\Pages;
303
+
304
+ use App\Filament\Resources\Customers\CustomerResource;
305
+ use Filament\Resources\Pages\CreateRecord;
306
+
307
+ class CreateCustomer extends CreateRecord
308
+ {
309
+ protected static string $resource = CustomerResource::class;
310
+
311
+ protected function getRedirectUrl(): string
312
+ {
313
+ return $this->getResource()::getUrl('index');
314
+ }
315
+
316
+ protected function mutateFormDataBeforeCreate(array $data): array
317
+ {
318
+ $data['created_by'] = auth()->id();
319
+ return $data;
320
+ }
321
+ }
322
+
323
+ // Pages/EditCustomer.php
324
+ <?php
325
+
326
+ namespace App\Filament\Resources\Customers\Pages;
327
+
328
+ use App\Filament\Resources\Customers\CustomerResource;
329
+ use Filament\Resources\Pages\EditRecord;
330
+ use Filament\Actions\DeleteAction;
331
+
332
+ class EditCustomer extends EditRecord
333
+ {
334
+ protected static string $resource = CustomerResource::class;
335
+
336
+ protected function getHeaderActions(): array
337
+ {
338
+ return [DeleteAction::make()];
339
+ }
340
+
341
+ protected function afterSave(): void
342
+ {
343
+ // Hook after record is saved
344
+ }
345
+ }
346
+ ```
347
+
348
+ ## Tables
349
+
350
+ ### Column Types
351
+
352
+ ```php
353
+ use Filament\Tables\Columns\TextColumn;
354
+ use Filament\Tables\Columns\IconColumn;
355
+ use Filament\Tables\Columns\ImageColumn;
356
+ use Filament\Tables\Columns\SelectColumn;
357
+ use Filament\Tables\Columns\ToggleColumn;
358
+ use Filament\Tables\Columns\CheckboxColumn;
359
+ use Filament\Tables\Columns\ColorColumn;
360
+ use Filament\Tables\Columns\TextInputColumn;
361
+
362
+ // Text column variants
363
+ TextColumn::make('name')->searchable()->sortable()->copyable()
364
+ TextColumn::make('email')->icon('heroicon-m-envelope')
365
+ TextColumn::make('created_at')->dateTime('M j, Y')->sortable()
366
+ TextColumn::make('amount')->money('USD')->sortable()
367
+ TextColumn::make('size')->formatStateUsing(fn (int $state): string => number_format($state / 1024, 2) . ' KB')
368
+ TextColumn::make('tags')->badge()->separator(',')
369
+ TextColumn::make('description')->limit(50)->tooltip(fn ($record) => $record->description)
370
+ TextColumn::make('status')->badge()->color(fn (string $state): string => match ($state) {
371
+ 'draft' => 'gray',
372
+ 'reviewing' => 'warning',
373
+ 'published' => 'success',
374
+ 'rejected' => 'danger',
375
+ })
376
+ TextColumn::make('author.name') // Dot-notation auto-eager-loads relationships
377
+ TextColumn::make('tags.name')->badge() // HasMany / BelongsToMany
378
+
379
+ // Other column types
380
+ IconColumn::make('is_featured')->boolean()
381
+ ImageColumn::make('avatar')->circular()->size(40)
382
+ ToggleColumn::make('is_active') // Inline toggle, updates DB immediately
383
+ SelectColumn::make('status')->options([...]) // Inline select
384
+ CheckboxColumn::make('is_verified')
385
+ ColorColumn::make('hex_color')
386
+ TextInputColumn::make('sort_order')->rules(['required', 'integer']) // Inline editable
387
+ ```
388
+
389
+ ### Column Formatting
390
+
391
+ ```php
392
+ TextColumn::make('price')->money('USD') // $1,234.56
393
+ TextColumn::make('views')->numeric(decimalPlaces: 0) // 1,234
394
+ TextColumn::make('created_at')->since() // 3 hours ago
395
+ TextColumn::make('birthday')->date('F j, Y') // January 1, 2000
396
+ TextColumn::make('duration')->time() // 12:30:00
397
+ TextColumn::make('total')
398
+ ->summarize([
399
+ Sum::make()->label('Total'),
400
+ Average::make()->label('Average'),
401
+ ])
402
+ ```
403
+
404
+ ### Column Toggleability
405
+
406
+ ```php
407
+ TextColumn::make('created_at')
408
+ ->toggleable() // User can show/hide
409
+ TextColumn::make('updated_at')
410
+ ->toggleable(isToggledHiddenByDefault: true) // Hidden by default
411
+ ```
412
+
413
+ ### Filters
414
+
415
+ ```php
416
+ use Filament\Tables\Filters\Filter;
417
+ use Filament\Tables\Filters\SelectFilter;
418
+ use Filament\Tables\Filters\TernaryFilter;
419
+ use Filament\Tables\Filters\QueryBuilder;
420
+ use Filament\Tables\Filters\QueryBuilder\Constraints\TextConstraint;
421
+ use Filament\Tables\Filters\QueryBuilder\Constraints\DateConstraint;
422
+ use Filament\Tables\Filters\QueryBuilder\Constraints\NumberConstraint;
423
+ use Illuminate\Database\Eloquent\Builder;
424
+
425
+ // Simple query filter
426
+ Filter::make('is_featured')
427
+ ->query(fn (Builder $query) => $query->where('is_featured', true))
428
+ ->toggle() // Shows as a toggle instead of checkbox
429
+
430
+ // Select filter
431
+ SelectFilter::make('status')
432
+ ->options([
433
+ 'draft' => 'Draft',
434
+ 'published' => 'Published',
435
+ 'archived' => 'Archived',
436
+ ])
437
+ ->multiple() // Allow selecting multiple values
438
+
439
+ // Relationship filter
440
+ SelectFilter::make('category')
441
+ ->relationship('category', 'name')
442
+ ->searchable()
443
+ ->preload()
444
+
445
+ // Ternary filter (yes / no / any)
446
+ TernaryFilter::make('email_verified')
447
+ ->nullable() // Uses whereNull / whereNotNull
448
+ ->label('Email verified')
449
+
450
+ // Custom form filter
451
+ Filter::make('created_at')
452
+ ->schema([
453
+ DatePicker::make('created_from'),
454
+ DatePicker::make('created_until'),
455
+ ])
456
+ ->query(function (Builder $query, array $data): Builder {
457
+ return $query
458
+ ->when($data['created_from'], fn (Builder $q, $date) => $q->whereDate('created_at', '>=', $date))
459
+ ->when($data['created_until'], fn (Builder $q, $date) => $q->whereDate('created_at', '<=', $date));
460
+ })
461
+ ->indicateUsing(function (array $data): array {
462
+ $indicators = [];
463
+ if ($data['created_from'] ?? null) {
464
+ $indicators[] = 'Created from ' . Carbon::parse($data['created_from'])->toFormattedDateString();
465
+ }
466
+ if ($data['created_until'] ?? null) {
467
+ $indicators[] = 'Created until ' . Carbon::parse($data['created_until'])->toFormattedDateString();
468
+ }
469
+ return $indicators;
470
+ })
471
+
472
+ // Advanced query builder (user-facing filter UI)
473
+ QueryBuilder::make()
474
+ ->constraints([
475
+ TextConstraint::make('name'),
476
+ DateConstraint::make('created_at'),
477
+ NumberConstraint::make('amount'),
478
+ ])
479
+ ```
480
+
481
+ ### Table Actions
482
+
483
+ ```php
484
+ use Filament\Actions\Action;
485
+ use Filament\Actions\EditAction;
486
+ use Filament\Actions\ViewAction;
487
+ use Filament\Actions\DeleteAction;
488
+ use Filament\Actions\ForceDeleteAction;
489
+ use Filament\Actions\RestoreAction;
490
+ use Filament\Actions\ReplicateAction;
491
+ use Filament\Actions\BulkActionGroup;
492
+ use Filament\Actions\DeleteBulkAction;
493
+ use Filament\Actions\ForceDeleteBulkAction;
494
+ use Filament\Actions\RestoreBulkAction;
495
+ use Filament\Actions\ExportAction;
496
+ use Filament\Actions\ImportAction;
497
+
498
+ $table
499
+ ->recordActions([
500
+ ViewAction::make(),
501
+ EditAction::make(),
502
+ DeleteAction::make(),
503
+ ReplicateAction::make()
504
+ ->excludeAttributes(['slug']),
505
+ Action::make('feature')
506
+ ->icon('heroicon-o-star')
507
+ ->action(fn (Post $record) => $record->update(['is_featured' => true]))
508
+ ->requiresConfirmation()
509
+ ->hidden(fn (Post $record): bool => $record->is_featured)
510
+ ->color('warning'),
511
+ ForceDeleteAction::make(),
512
+ RestoreAction::make(),
513
+ ])
514
+ ->toolbarActions([
515
+ BulkActionGroup::make([
516
+ DeleteBulkAction::make(),
517
+ ForceDeleteBulkAction::make(),
518
+ RestoreBulkAction::make(),
519
+ ExportAction::make()->exporter(CustomerExporter::class),
520
+ ]),
521
+ ImportAction::make()->importer(CustomerImporter::class),
522
+ ])
523
+ ```
524
+
525
+ ### Table Configuration
526
+
527
+ ```php
528
+ $table
529
+ ->paginated([10, 25, 50, 100])
530
+ ->defaultPaginationPageOption(25)
531
+ ->defaultSort('created_at', 'desc')
532
+ ->striped()
533
+ ->deferLoading()
534
+ ->poll('10s')
535
+ ->reorderable('sort')
536
+ ->heading('Clients')
537
+ ->description('Manage your clients')
538
+ ->emptyStateHeading('No clients yet')
539
+ ->emptyStateDescription('Create your first client to get started.')
540
+ ->emptyStateIcon('heroicon-o-users')
541
+ ->groups([
542
+ Group::make('status')->collapsible(),
543
+ Group::make('category.name')->label('Category'),
544
+ ])
545
+ ->modifyQueryUsing(fn (Builder $query) => $query->withoutTrashed())
546
+ ->recordUrl(fn (Model $record): string => route('customers.show', $record))
547
+ ->recordClasses(fn (Model $record) => match ($record->status) {
548
+ 'blocked' => 'opacity-50',
549
+ default => '',
550
+ })
551
+ ```
552
+
553
+ ## Forms
554
+
555
+ ### Field Types
556
+
557
+ ```php
558
+ use Filament\Forms\Components\TextInput;
559
+ use Filament\Forms\Components\Textarea;
560
+ use Filament\Forms\Components\Select;
561
+ use Filament\Forms\Components\Checkbox;
562
+ use Filament\Forms\Components\Toggle;
563
+ use Filament\Forms\Components\CheckboxList;
564
+ use Filament\Forms\Components\Radio;
565
+ use Filament\Forms\Components\DatePicker;
566
+ use Filament\Forms\Components\DateTimePicker;
567
+ use Filament\Forms\Components\TimePicker;
568
+ use Filament\Forms\Components\FileUpload;
569
+ use Filament\Forms\Components\RichEditor;
570
+ use Filament\Forms\Components\MarkdownEditor;
571
+ use Filament\Forms\Components\Repeater;
572
+ use Filament\Forms\Components\Builder;
573
+ use Filament\Forms\Components\TagsInput;
574
+ use Filament\Forms\Components\KeyValue;
575
+ use Filament\Forms\Components\ColorPicker;
576
+ use Filament\Forms\Components\ToggleButtons;
577
+ use Filament\Forms\Components\Hidden;
578
+ use Filament\Forms\Components\Slider;
579
+ use Filament\Forms\Components\CodeEditor;
580
+ ```
581
+
582
+ ### TextInput Patterns
583
+
584
+ ```php
585
+ TextInput::make('name')
586
+ ->required()
587
+ ->maxLength(255)
588
+ ->autofocus()
589
+ ->placeholder('Enter customer name')
590
+
591
+ TextInput::make('slug')
592
+ ->required()
593
+ ->unique(ignoreRecord: true)
594
+ ->alphaDash()
595
+
596
+ TextInput::make('price')
597
+ ->numeric()
598
+ ->prefix('$')
599
+ ->step(0.01)
600
+ ->minValue(0)
601
+ ->maxValue(999999.99)
602
+
603
+ TextInput::make('email')
604
+ ->email()
605
+ ->required()
606
+ ->unique(ignoreRecord: true)
607
+
608
+ TextInput::make('phone')
609
+ ->tel()
610
+ ->mask('(999) 999-9999')
611
+
612
+ TextInput::make('password')
613
+ ->password()
614
+ ->revealable()
615
+ ->confirmed() // Requires a password_confirmation field
616
+ ->dehydrateStateUsing(fn (string $state): string => Hash::make($state))
617
+ ->dehydrated(fn (?string $state): bool => filled($state)) // Skip if empty on edit
618
+ ->required(fn (string $operation): bool => $operation === 'create')
619
+ ```
620
+
621
+ ### Select Patterns
622
+
623
+ ```php
624
+ // Static options
625
+ Select::make('status')
626
+ ->options([
627
+ 'draft' => 'Draft',
628
+ 'reviewing' => 'Reviewing',
629
+ 'published' => 'Published',
630
+ ])
631
+ ->default('draft')
632
+ ->required()
633
+
634
+ // From relationship
635
+ Select::make('category_id')
636
+ ->relationship('category', 'name')
637
+ ->searchable()
638
+ ->preload()
639
+ ->createOptionForm([
640
+ TextInput::make('name')->required(),
641
+ TextInput::make('slug')->required(),
642
+ ])
643
+ ->editOptionForm([
644
+ TextInput::make('name')->required(),
645
+ TextInput::make('slug')->required(),
646
+ ])
647
+
648
+ // Multiple select
649
+ Select::make('tags')
650
+ ->multiple()
651
+ ->relationship('tags', 'name')
652
+ ->preload()
653
+
654
+ // Dependent selects
655
+ Select::make('country_id')
656
+ ->options(Country::pluck('name', 'id'))
657
+ ->live()
658
+ ->afterStateUpdated(fn (Set $set) => $set('city_id', null))
659
+
660
+ Select::make('city_id')
661
+ ->options(fn (Get $get): array =>
662
+ City::where('country_id', $get('country_id'))->pluck('name', 'id')->toArray())
663
+ ->searchable()
664
+ ```
665
+
666
+ ### Date & Time
667
+
668
+ ```php
669
+ DatePicker::make('published_at')
670
+ ->native(false) // Use Filament date picker instead of browser native
671
+ ->default(now())
672
+ ->minDate(now())
673
+ ->maxDate(now()->addYear())
674
+ ->displayFormat('d/m/Y')
675
+ ->closeOnDateSelection()
676
+
677
+ DateTimePicker::make('starts_at')
678
+ ->native(false)
679
+ ->seconds(false) // Hide seconds
680
+ ->minutesStep(15) // 15-minute intervals
681
+ ->timezone('America/New_York')
682
+
683
+ TimePicker::make('opening_time')
684
+ ->native(false)
685
+ ->seconds(false)
686
+ ```
687
+
688
+ ### File Uploads
689
+
690
+ ```php
691
+ FileUpload::make('avatar')
692
+ ->image()
693
+ ->directory('avatars')
694
+ ->disk('public')
695
+ ->imageEditor()
696
+ ->imageResizeMode('cover')
697
+ ->imageCropAspectRatio('1:1')
698
+ ->imageResizeTargetWidth('300')
699
+ ->imageResizeTargetHeight('300')
700
+ ->maxSize(2048) // 2MB in KB
701
+
702
+ FileUpload::make('attachments')
703
+ ->multiple()
704
+ ->directory('attachments')
705
+ ->acceptedFileTypes(['application/pdf', 'image/*'])
706
+ ->maxFiles(5)
707
+ ->reorderable()
708
+ ->downloadable()
709
+ ->openable()
710
+ ->previewable()
711
+ ```
712
+
713
+ ### Rich Text & Markdown
714
+
715
+ ```php
716
+ RichEditor::make('body')
717
+ ->required()
718
+ ->columnSpanFull()
719
+ ->toolbarButtons([
720
+ 'bold', 'italic', 'underline', 'strike',
721
+ 'h2', 'h3',
722
+ 'bulletList', 'orderedList',
723
+ 'link', 'blockquote', 'codeBlock',
724
+ 'attachFiles',
725
+ ])
726
+ ->fileAttachmentsDisk('public')
727
+ ->fileAttachmentsDirectory('attachments')
728
+
729
+ MarkdownEditor::make('content')
730
+ ->columnSpanFull()
731
+ ->toolbarButtons([
732
+ 'bold', 'italic', 'strike',
733
+ 'heading', 'bulletList', 'orderedList',
734
+ 'link', 'codeBlock', 'attachFiles',
735
+ ])
736
+ ```
737
+
738
+ ### Repeater & Builder
739
+
740
+ ```php
741
+ // Repeater -- ordered list of identical items
742
+ Repeater::make('line_items')
743
+ ->schema([
744
+ Select::make('product_id')
745
+ ->options(Product::pluck('name', 'id'))
746
+ ->required()
747
+ ->columnSpan(2),
748
+ TextInput::make('quantity')
749
+ ->numeric()
750
+ ->default(1)
751
+ ->minValue(1)
752
+ ->required(),
753
+ TextInput::make('unit_price')
754
+ ->numeric()
755
+ ->prefix('$')
756
+ ->required(),
757
+ ])
758
+ ->columns(4)
759
+ ->defaultItems(1)
760
+ ->addActionLabel('Add line item')
761
+ ->reorderable()
762
+ ->collapsible()
763
+ ->cloneable()
764
+ ->minItems(1)
765
+ ->maxItems(20)
766
+ ->itemLabel(fn (array $state): ?string => $state['product_id'] ?? null)
767
+
768
+ // Builder -- flexible block-based content
769
+ Builder::make('content')
770
+ ->blocks([
771
+ Builder\Block::make('heading')
772
+ ->schema([
773
+ TextInput::make('content')->required(),
774
+ Select::make('level')
775
+ ->options(['h2' => 'H2', 'h3' => 'H3', 'h4' => 'H4'])
776
+ ->default('h2'),
777
+ ]),
778
+ Builder\Block::make('paragraph')
779
+ ->schema([
780
+ RichEditor::make('content')->required(),
781
+ ]),
782
+ Builder\Block::make('image')
783
+ ->schema([
784
+ FileUpload::make('url')->image()->required(),
785
+ TextInput::make('alt')->required(),
786
+ ]),
787
+ Builder\Block::make('call_to_action')
788
+ ->schema([
789
+ TextInput::make('label')->required(),
790
+ TextInput::make('url')->url()->required(),
791
+ ]),
792
+ ])
793
+ ->collapsible()
794
+ ->blockNumbers(false)
795
+ ```
796
+
797
+ ### Other Field Types
798
+
799
+ ```php
800
+ Toggle::make('is_active')
801
+ ->label('Active')
802
+ ->default(true)
803
+ ->onColor('success')
804
+ ->offColor('danger')
805
+
806
+ ToggleButtons::make('status')
807
+ ->inline()
808
+ ->options([
809
+ 'draft' => 'Draft',
810
+ 'published' => 'Published',
811
+ 'archived' => 'Archived',
812
+ ])
813
+ ->colors([
814
+ 'draft' => 'info',
815
+ 'published' => 'success',
816
+ 'archived' => 'warning',
817
+ ])
818
+ ->icons([
819
+ 'draft' => 'heroicon-o-pencil',
820
+ 'published' => 'heroicon-o-check-circle',
821
+ 'archived' => 'heroicon-o-archive-box',
822
+ ])
823
+
824
+ CheckboxList::make('roles')
825
+ ->relationship('roles', 'name')
826
+ ->columns(2)
827
+
828
+ Radio::make('plan')
829
+ ->options([
830
+ 'free' => 'Free',
831
+ 'pro' => 'Pro',
832
+ 'enterprise' => 'Enterprise',
833
+ ])
834
+ ->descriptions([
835
+ 'free' => 'Up to 5 projects',
836
+ 'pro' => 'Unlimited projects',
837
+ 'enterprise' => 'Custom limits',
838
+ ])
839
+
840
+ TagsInput::make('tags')
841
+ ->separator(',')
842
+ ->suggestions(['Laravel', 'PHP', 'Filament'])
843
+
844
+ KeyValue::make('metadata')
845
+ ->keyLabel('Key')
846
+ ->valueLabel('Value')
847
+ ->reorderable()
848
+
849
+ ColorPicker::make('color')
850
+ ->hex()
851
+
852
+ Hidden::make('user_id')
853
+ ->default(auth()->id())
854
+ ```
855
+
856
+ ### Reactive Fields (live)
857
+
858
+ ```php
859
+ use Filament\Forms\Get;
860
+ use Filament\Forms\Set;
861
+
862
+ TextInput::make('title')
863
+ ->live(debounce: 500) // Debounce server round-trips by 500ms
864
+ ->afterStateUpdated(fn (Set $set, ?string $state) =>
865
+ $set('slug', Str::slug($state)))
866
+
867
+ TextInput::make('slug')
868
+ ->required()
869
+ ->unique(ignoreRecord: true)
870
+
871
+ // Conditional visibility (server-side)
872
+ TextInput::make('other_reason')
873
+ ->visible(fn (Get $get): bool => $get('reason') === 'other')
874
+
875
+ // JS-side reactivity (no server round-trip)
876
+ Toggle::make('is_admin')
877
+ TextInput::make('admin_notes')
878
+ ->hiddenJs(<<<'JS' $get('is_admin') !== true JS)
879
+
880
+ // Calculated fields
881
+ TextInput::make('quantity')->numeric()->live()
882
+ TextInput::make('unit_price')->numeric()->live()
883
+ TextInput::make('total')
884
+ ->numeric()
885
+ ->readOnly()
886
+ ->dehydrated(false)
887
+ ->afterStateHydrated(function (Get $get, Set $set) {
888
+ $set('total', ($get('quantity') ?? 0) * ($get('unit_price') ?? 0));
889
+ })
890
+ ```
891
+
892
+ ### Utility Injection Parameters
893
+
894
+ Closures used in field configuration receive named parameters via dependency injection:
895
+
896
+ | Parameter | Type | Description |
897
+ |-----------|------|-------------|
898
+ | `$state` | `mixed` | Current field value |
899
+ | `$get` | `Get` | Get sibling field values: `$get('field_name')` |
900
+ | `$set` | `Set` | Set sibling field values: `$set('field_name', $value)` |
901
+ | `$record` | `?Model` | Current Eloquent record (null on create) |
902
+ | `$operation` | `string` | `'create'`, `'edit'`, or `'view'` |
903
+ | `$livewire` | `Component` | Parent Livewire component instance |
904
+ | `$component` | `Component` | The field component instance itself |
905
+ | `$model` | `string` | Model class name |
906
+
907
+ ### Validation
908
+
909
+ ```php
910
+ TextInput::make('name')
911
+ ->required()
912
+ ->minLength(2)
913
+ ->maxLength(255)
914
+ ->rules(['alpha_dash'])
915
+ ->unique(table: 'users', column: 'name', ignoreRecord: true)
916
+ ->exists(table: 'categories', column: 'slug')
917
+ ->requiredWith('other_field')
918
+ ->prohibitedUnless('status', 'active')
919
+
920
+ // Custom validation
921
+ TextInput::make('code')
922
+ ->rules([
923
+ fn (): \Closure => function (string $attribute, $value, \Closure $fail) {
924
+ if (! preg_match('/^[A-Z]{3}-\d{4}$/', $value)) {
925
+ $fail('The code must match the format XXX-0000.');
926
+ }
927
+ },
928
+ ])
929
+ ```
930
+
931
+ ## Schemas (Layouts)
932
+
933
+ ```php
934
+ use Filament\Schemas\Components\Grid;
935
+ use Filament\Schemas\Components\Flex;
936
+ use Filament\Schemas\Components\Section;
937
+ use Filament\Schemas\Components\Tabs;
938
+ use Filament\Schemas\Components\Wizard;
939
+ use Filament\Schemas\Components\Fieldset;
940
+ use Filament\Schemas\Components\Callout;
941
+ use Filament\Schemas\Components\EmptyState;
942
+
943
+ // Grid layout
944
+ Grid::make(2)->schema([
945
+ TextInput::make('first_name'),
946
+ TextInput::make('last_name'),
947
+ ])
948
+
949
+ // Columns on individual fields
950
+ TextInput::make('description')->columnSpan(2)
951
+ TextInput::make('notes')->columnSpanFull()
952
+
953
+ // Section with header
954
+ Section::make('Contact Information')
955
+ ->description('How can we reach this customer?')
956
+ ->schema([...])
957
+ ->collapsible()
958
+ ->collapsed() // Start collapsed
959
+ ->icon('heroicon-o-phone')
960
+ ->columns(2)
961
+ ->aside() // Show section content beside the header
962
+
963
+ // Tabs
964
+ Tabs::make('Settings')
965
+ ->tabs([
966
+ Tabs\Tab::make('General')
967
+ ->icon('heroicon-o-cog-6-tooth')
968
+ ->schema([...]),
969
+ Tabs\Tab::make('Security')
970
+ ->icon('heroicon-o-lock-closed')
971
+ ->schema([...])
972
+ ->badge(5), // Show badge count
973
+ Tabs\Tab::make('Notifications')
974
+ ->schema([...]),
975
+ ])
976
+ ->persistTabInQueryString() // Remember selected tab in URL
977
+
978
+ // Wizard (multi-step form)
979
+ Wizard::make([
980
+ Wizard\Step::make('Account')
981
+ ->description('Set up your account')
982
+ ->schema([
983
+ TextInput::make('email')->email()->required(),
984
+ TextInput::make('password')->password()->required(),
985
+ ])
986
+ ->icon('heroicon-o-user'),
987
+ Wizard\Step::make('Profile')
988
+ ->description('Tell us about yourself')
989
+ ->schema([
990
+ TextInput::make('name')->required(),
991
+ FileUpload::make('avatar')->image(),
992
+ ]),
993
+ Wizard\Step::make('Billing')
994
+ ->schema([...]),
995
+ ])
996
+ ->skippable() // Allow skipping steps
997
+ ->submitAction(view('filament.wizard-submit'))
998
+
999
+ // Fieldset
1000
+ Fieldset::make('Address')
1001
+ ->schema([
1002
+ TextInput::make('street'),
1003
+ TextInput::make('city'),
1004
+ TextInput::make('state'),
1005
+ TextInput::make('zip'),
1006
+ ])
1007
+ ->columns(2)
1008
+
1009
+ // Callout
1010
+ Callout::make()
1011
+ ->info()
1012
+ ->content('This resource is read-only because the record is archived.')
1013
+ ```
1014
+
1015
+ ## Actions & Modals
1016
+
1017
+ ```php
1018
+ use Filament\Actions\Action;
1019
+
1020
+ // Simple confirmation
1021
+ Action::make('delete')
1022
+ ->requiresConfirmation()
1023
+ ->modalHeading('Delete record')
1024
+ ->modalDescription('Are you sure you want to delete this record? This cannot be undone.')
1025
+ ->modalSubmitActionLabel('Yes, delete')
1026
+ ->action(fn () => $this->record->delete())
1027
+ ->color('danger')
1028
+ ->icon('heroicon-o-trash')
1029
+
1030
+ // With modal form
1031
+ Action::make('sendEmail')
1032
+ ->icon('heroicon-o-envelope')
1033
+ ->schema([
1034
+ TextInput::make('subject')->required(),
1035
+ RichEditor::make('body')->required(),
1036
+ ])
1037
+ ->action(function (array $data) {
1038
+ Mail::to($this->record->email)->send(
1039
+ new GenericEmail($data['subject'], $data['body'])
1040
+ );
1041
+ Notification::make()->title('Email sent')->success()->send();
1042
+ })
1043
+
1044
+ // Button styles
1045
+ Action::make('save')->button()
1046
+ Action::make('edit')->link()
1047
+ Action::make('delete')->iconButton()->icon('heroicon-m-trash')
1048
+ Action::make('info')->badge()
1049
+ Action::make('export')->outlined()
1050
+
1051
+ // Grouped actions
1052
+ \Filament\Actions\ActionGroup::make([
1053
+ Action::make('view'),
1054
+ Action::make('edit'),
1055
+ Action::make('delete'),
1056
+ ])->icon('heroicon-m-ellipsis-vertical')
1057
+
1058
+ // Action with custom URL
1059
+ Action::make('visit')
1060
+ ->url(fn (): string => route('posts.show', $this->record))
1061
+ ->openUrlInNewTab()
1062
+
1063
+ // Header actions on pages
1064
+ protected function getHeaderActions(): array
1065
+ {
1066
+ return [
1067
+ Action::make('export')
1068
+ ->action(fn () => $this->export())
1069
+ ->color('gray'),
1070
+ \Filament\Actions\CreateAction::make(),
1071
+ ];
1072
+ }
1073
+ ```
1074
+
1075
+ ## Widgets & Dashboard
1076
+
1077
+ ### Stats Overview
1078
+
1079
+ ```php
1080
+ use Filament\Widgets\StatsOverviewWidget;
1081
+ use Filament\Widgets\StatsOverviewWidget\Stat;
1082
+
1083
+ class StatsOverview extends StatsOverviewWidget
1084
+ {
1085
+ protected static ?int $sort = 1;
1086
+
1087
+ protected function getStats(): array
1088
+ {
1089
+ return [
1090
+ Stat::make('Total Revenue', '$' . number_format(Order::sum('total'), 2))
1091
+ ->description('32% increase')
1092
+ ->descriptionIcon('heroicon-m-arrow-trending-up')
1093
+ ->color('success')
1094
+ ->chart([7, 2, 10, 3, 15, 4, 17]),
1095
+ Stat::make('New Customers', Customer::whereMonth('created_at', now()->month)->count())
1096
+ ->description('3% decrease')
1097
+ ->descriptionIcon('heroicon-m-arrow-trending-down')
1098
+ ->color('danger'),
1099
+ Stat::make('Active Orders', Order::where('status', 'processing')->count())
1100
+ ->description('21 shipped today')
1101
+ ->color('info'),
1102
+ ];
1103
+ }
1104
+ }
1105
+ ```
1106
+
1107
+ ### Chart Widget
1108
+
1109
+ ```php
1110
+ use Filament\Widgets\ChartWidget;
1111
+
1112
+ class RevenueChart extends ChartWidget
1113
+ {
1114
+ protected static ?string $heading = 'Monthly Revenue';
1115
+ protected static ?int $sort = 2;
1116
+ protected static ?string $maxHeight = '300px';
1117
+ protected int|string|array $columnSpan = 'full';
1118
+
1119
+ protected function getData(): array
1120
+ {
1121
+ $data = Order::selectRaw('MONTH(created_at) as month, SUM(total) as revenue')
1122
+ ->whereYear('created_at', now()->year)
1123
+ ->groupBy('month')
1124
+ ->pluck('revenue', 'month')
1125
+ ->toArray();
1126
+
1127
+ return [
1128
+ 'datasets' => [
1129
+ [
1130
+ 'label' => 'Revenue',
1131
+ 'data' => array_values($data),
1132
+ 'backgroundColor' => '#36A2EB',
1133
+ 'borderColor' => '#36A2EB',
1134
+ ],
1135
+ ],
1136
+ 'labels' => ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun',
1137
+ 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'],
1138
+ ];
1139
+ }
1140
+
1141
+ protected function getType(): string
1142
+ {
1143
+ return 'line'; // bar, pie, doughnut, radar, polarArea, bubble, scatter
1144
+ }
1145
+
1146
+ protected function getOptions(): array
1147
+ {
1148
+ return [
1149
+ 'scales' => [
1150
+ 'y' => ['beginAtZero' => true],
1151
+ ],
1152
+ ];
1153
+ }
1154
+ }
1155
+ ```
1156
+
1157
+ ### Table Widget
1158
+
1159
+ ```php
1160
+ use Filament\Widgets\TableWidget;
1161
+
1162
+ class LatestOrders extends TableWidget
1163
+ {
1164
+ protected static ?int $sort = 3;
1165
+ protected int|string|array $columnSpan = 'full';
1166
+
1167
+ public function table(Table $table): Table
1168
+ {
1169
+ return $table
1170
+ ->query(Order::query()->latest()->limit(5))
1171
+ ->columns([
1172
+ TextColumn::make('number')->label('Order #'),
1173
+ TextColumn::make('customer.name'),
1174
+ TextColumn::make('total')->money('USD'),
1175
+ TextColumn::make('status')->badge(),
1176
+ TextColumn::make('created_at')->since(),
1177
+ ])
1178
+ ->paginated(false);
1179
+ }
1180
+ }
1181
+ ```
1182
+
1183
+ ### Dashboard Filters
1184
+
1185
+ ```php
1186
+ use Filament\Pages\Dashboard as BaseDashboard;
1187
+ use Filament\Pages\Dashboard\Actions\FilterAction;
1188
+ use Filament\Pages\Dashboard\Concerns\HasFiltersAction;
1189
+ use Filament\Forms\Components\DatePicker;
1190
+ use Filament\Forms\Components\Select;
1191
+
1192
+ class Dashboard extends BaseDashboard
1193
+ {
1194
+ use HasFiltersAction;
1195
+
1196
+ protected function getHeaderActions(): array
1197
+ {
1198
+ return [
1199
+ FilterAction::make()->schema([
1200
+ DatePicker::make('startDate')->default(now()->subMonth()),
1201
+ DatePicker::make('endDate')->default(now()),
1202
+ Select::make('status')
1203
+ ->options(['all' => 'All', 'active' => 'Active', 'inactive' => 'Inactive'])
1204
+ ->default('all'),
1205
+ ]),
1206
+ ];
1207
+ }
1208
+ }
1209
+
1210
+ // Consuming filters in a widget
1211
+ use Filament\Widgets\Concerns\InteractsWithPageFilters;
1212
+
1213
+ class RevenueWidget extends StatsOverviewWidget
1214
+ {
1215
+ use InteractsWithPageFilters;
1216
+
1217
+ protected function getStats(): array
1218
+ {
1219
+ $start = $this->pageFilters['startDate'] ?? now()->subMonth();
1220
+ $end = $this->pageFilters['endDate'] ?? now();
1221
+
1222
+ $revenue = Order::whereBetween('created_at', [$start, $end])->sum('total');
1223
+
1224
+ return [
1225
+ Stat::make('Revenue', '$' . number_format($revenue, 2)),
1226
+ ];
1227
+ }
1228
+ }
1229
+ ```
1230
+
1231
+ ### Widget Placement
1232
+
1233
+ ```php
1234
+ // In resource class -- attach widgets to resource pages
1235
+ public static function getWidgets(): array
1236
+ {
1237
+ return [CustomerStatsWidget::class];
1238
+ }
1239
+
1240
+ // In ListRecords page
1241
+ protected function getHeaderWidgets(): array
1242
+ {
1243
+ return [CustomerStatsWidget::class];
1244
+ }
1245
+
1246
+ // Widget column span
1247
+ protected int|string|array $columnSpan = 'full'; // or 1, 2, etc.
1248
+ ```
1249
+
1250
+ ## Multi-Tenancy
1251
+
1252
+ ```php
1253
+ // Panel configuration
1254
+ $panel->tenant(Team::class, slugAttribute: 'slug')
1255
+ ->tenantRegistration(RegisterTeam::class)
1256
+ ->tenantProfile(EditTeamProfile::class)
1257
+ ->requiresTenantSubscription()
1258
+ ->tenantMenu(true)
1259
+ ->tenantMiddleware([
1260
+ ApplyTenantScopes::class,
1261
+ ]);
1262
+
1263
+ // User model
1264
+ use Filament\Models\Contracts\HasTenants;
1265
+ use Filament\Panel;
1266
+ use Illuminate\Database\Eloquent\Model;
1267
+ use Illuminate\Support\Collection;
1268
+
1269
+ class User extends Authenticatable implements HasTenants
1270
+ {
1271
+ public function teams(): BelongsToMany
1272
+ {
1273
+ return $this->belongsToMany(Team::class);
1274
+ }
1275
+
1276
+ public function getTenants(Panel $panel): Collection
1277
+ {
1278
+ return $this->teams;
1279
+ }
1280
+
1281
+ public function canAccessTenant(Model $tenant): bool
1282
+ {
1283
+ return $this->teams()->whereKey($tenant)->exists();
1284
+ }
1285
+ }
1286
+
1287
+ // Accessing current tenant anywhere
1288
+ use Filament\Facades\Filament;
1289
+
1290
+ $tenant = Filament::getTenant();
1291
+
1292
+ // Scoping resource queries to tenant
1293
+ // In resource:
1294
+ public static function getEloquentQuery(): Builder
1295
+ {
1296
+ return parent::getEloquentQuery()->whereBelongsTo(Filament::getTenant());
1297
+ }
1298
+
1299
+ // Or use the global scope approach via TenantScope
1300
+ ```
1301
+
1302
+ ## Infolists (Read-Only Display)
1303
+
1304
+ ```php
1305
+ use Filament\Infolists\Components\TextEntry;
1306
+ use Filament\Infolists\Components\IconEntry;
1307
+ use Filament\Infolists\Components\ImageEntry;
1308
+ use Filament\Infolists\Components\ColorEntry;
1309
+ use Filament\Infolists\Components\RepeatableEntry;
1310
+ use Filament\Infolists\Components\KeyValueEntry;
1311
+
1312
+ // In resource or ViewRecord page
1313
+ public static function infolist(Infolist $infolist): Infolist
1314
+ {
1315
+ return $infolist->schema([
1316
+ Section::make('Customer Details')->schema([
1317
+ TextEntry::make('name'),
1318
+ TextEntry::make('email')
1319
+ ->copyable()
1320
+ ->icon('heroicon-m-envelope'),
1321
+ TextEntry::make('status')
1322
+ ->badge()
1323
+ ->color(fn (string $state) => match ($state) {
1324
+ 'active' => 'success',
1325
+ 'inactive' => 'gray',
1326
+ 'blocked' => 'danger',
1327
+ }),
1328
+ IconEntry::make('is_vip')->boolean(),
1329
+ ImageEntry::make('avatar')->circular(),
1330
+ TextEntry::make('created_at')->dateTime(),
1331
+ ])->columns(2),
1332
+
1333
+ Section::make('Orders')->schema([
1334
+ RepeatableEntry::make('orders')->schema([
1335
+ TextEntry::make('number'),
1336
+ TextEntry::make('total')->money('USD'),
1337
+ TextEntry::make('status')->badge(),
1338
+ ])->columns(3),
1339
+ ]),
1340
+
1341
+ KeyValueEntry::make('metadata'),
1342
+ ]);
1343
+ }
1344
+ ```
1345
+
1346
+ ## Notifications
1347
+
1348
+ ```php
1349
+ use Filament\Notifications\Notification;
1350
+
1351
+ // Success notification
1352
+ Notification::make()
1353
+ ->title('Saved successfully')
1354
+ ->success()
1355
+ ->send();
1356
+
1357
+ // Error notification
1358
+ Notification::make()
1359
+ ->title('Error occurred')
1360
+ ->body('Could not save the record. Please try again.')
1361
+ ->danger()
1362
+ ->persistent() // Does not auto-dismiss
1363
+ ->send();
1364
+
1365
+ // Warning with action
1366
+ Notification::make()
1367
+ ->title('Rate limit approaching')
1368
+ ->body('You have used 90% of your API quota.')
1369
+ ->warning()
1370
+ ->actions([
1371
+ \Filament\Notifications\Actions\Action::make('upgrade')
1372
+ ->button()
1373
+ ->url(route('billing')),
1374
+ \Filament\Notifications\Actions\Action::make('dismiss')
1375
+ ->close(),
1376
+ ])
1377
+ ->send();
1378
+
1379
+ // Database notifications (persisted, shown in bell icon)
1380
+ Notification::make()
1381
+ ->title('New order received')
1382
+ ->body("Order #{$order->number} was placed by {$order->customer->name}.")
1383
+ ->icon('heroicon-o-shopping-cart')
1384
+ ->sendToDatabase($user);
1385
+
1386
+ // Broadcast (real-time via websockets)
1387
+ Notification::make()
1388
+ ->title('Deployment complete')
1389
+ ->success()
1390
+ ->broadcast($user);
1391
+ ```
1392
+
1393
+ ## Navigation
1394
+
1395
+ ```php
1396
+ // Resource / Page navigation properties
1397
+ protected static ?string $navigationIcon = 'heroicon-o-document-text';
1398
+ protected static ?string $navigationGroup = 'Content';
1399
+ protected static ?int $navigationSort = 3;
1400
+ protected static ?string $navigationLabel = 'Articles';
1401
+ protected static ?string $navigationParentItem = 'Content';
1402
+
1403
+ // Dynamic navigation badge
1404
+ public static function getNavigationBadge(): ?string
1405
+ {
1406
+ return static::getModel()::count();
1407
+ }
1408
+
1409
+ public static function getNavigationBadgeColor(): ?string
1410
+ {
1411
+ return static::getModel()::count() > 10 ? 'warning' : 'primary';
1412
+ }
1413
+
1414
+ // Clusters (group of related pages)
1415
+ use Filament\Clusters\Cluster;
1416
+
1417
+ class Settings extends Cluster
1418
+ {
1419
+ protected static ?string $navigationIcon = 'heroicon-o-cog-6-tooth';
1420
+ }
1421
+
1422
+ // Page inside a cluster
1423
+ class GeneralSettings extends Page
1424
+ {
1425
+ protected static ?string $cluster = Settings::class;
1426
+ }
1427
+
1428
+ // Custom navigation items in panel provider
1429
+ $panel->navigationItems([
1430
+ NavigationItem::make('Documentation')
1431
+ ->url('https://docs.example.com', shouldOpenInNewTab: true)
1432
+ ->icon('heroicon-o-book-open')
1433
+ ->group('Support')
1434
+ ->sort(10),
1435
+ ])
1436
+
1437
+ // User menu items
1438
+ $panel->userMenuItems([
1439
+ MenuItem::make()
1440
+ ->label('Profile')
1441
+ ->url(fn () => route('profile'))
1442
+ ->icon('heroicon-o-user'),
1443
+ ])
1444
+ ```
1445
+
1446
+ ## Custom Pages
1447
+
1448
+ ```php
1449
+ use Filament\Pages\Page;
1450
+
1451
+ class Settings extends Page
1452
+ {
1453
+ protected static ?string $navigationIcon = 'heroicon-o-cog-6-tooth';
1454
+ protected static string $view = 'filament.pages.settings';
1455
+ protected static ?string $title = 'Application Settings';
1456
+
1457
+ public ?array $data = [];
1458
+
1459
+ public function mount(): void
1460
+ {
1461
+ $this->form->fill([
1462
+ 'site_name' => config('app.name'),
1463
+ 'support_email' => config('mail.from.address'),
1464
+ ]);
1465
+ }
1466
+
1467
+ public function form(Schema $schema): Schema
1468
+ {
1469
+ return $schema->components([
1470
+ Section::make('General')->schema([
1471
+ TextInput::make('site_name')->required(),
1472
+ TextInput::make('support_email')->email()->required(),
1473
+ ]),
1474
+ ])->statePath('data');
1475
+ }
1476
+
1477
+ public function save(): void
1478
+ {
1479
+ $data = $this->form->getState();
1480
+ // Persist settings...
1481
+ Notification::make()->title('Settings saved')->success()->send();
1482
+ }
1483
+
1484
+ protected function getHeaderActions(): array
1485
+ {
1486
+ return [
1487
+ Action::make('save')
1488
+ ->action('save')
1489
+ ->button(),
1490
+ ];
1491
+ }
1492
+ }
1493
+ ```
1494
+
1495
+ ## Relation Managers
1496
+
1497
+ ```php
1498
+ use Filament\Resources\RelationManagers\RelationManager;
1499
+
1500
+ class OrdersRelationManager extends RelationManager
1501
+ {
1502
+ protected static string $relationship = 'orders';
1503
+ protected static ?string $recordTitleAttribute = 'number';
1504
+
1505
+ public function table(Table $table): Table
1506
+ {
1507
+ return $table
1508
+ ->columns([
1509
+ TextColumn::make('number'),
1510
+ TextColumn::make('total')->money('USD'),
1511
+ TextColumn::make('status')->badge(),
1512
+ TextColumn::make('created_at')->dateTime(),
1513
+ ])
1514
+ ->recordActions([
1515
+ EditAction::make(),
1516
+ DeleteAction::make(),
1517
+ ])
1518
+ ->toolbarActions([
1519
+ \Filament\Actions\CreateAction::make(),
1520
+ BulkActionGroup::make([
1521
+ DeleteBulkAction::make(),
1522
+ ]),
1523
+ ]);
1524
+ }
1525
+
1526
+ public function form(Schema $schema): Schema
1527
+ {
1528
+ return $schema->components([
1529
+ TextInput::make('number')->required(),
1530
+ TextInput::make('total')->numeric()->prefix('$')->required(),
1531
+ Select::make('status')
1532
+ ->options(['pending' => 'Pending', 'processing' => 'Processing', 'shipped' => 'Shipped'])
1533
+ ->required(),
1534
+ ]);
1535
+ }
1536
+ }
1537
+ ```
1538
+
1539
+ ## Authorization
1540
+
1541
+ Filament maps to Laravel model policies automatically:
1542
+
1543
+ | Policy Method | Controls |
1544
+ |---------------|----------|
1545
+ | `viewAny` | Can see resource in navigation and list page |
1546
+ | `view` | Can see view page / view action |
1547
+ | `create` | Can see create button and create page |
1548
+ | `update` | Can see edit button and edit page |
1549
+ | `delete` | Can delete individual records |
1550
+ | `deleteAny` | Can bulk-delete records |
1551
+ | `forceDelete` | Can force-delete soft-deleted records |
1552
+ | `forceDeleteAny` | Can bulk force-delete |
1553
+ | `restore` | Can restore soft-deleted records |
1554
+ | `restoreAny` | Can bulk-restore |
1555
+ | `reorder` | Can reorder records |
1556
+
1557
+ ```php
1558
+ // app/Policies/PostPolicy.php
1559
+ class PostPolicy
1560
+ {
1561
+ public function viewAny(User $user): bool
1562
+ {
1563
+ return $user->hasPermissionTo('view posts');
1564
+ }
1565
+
1566
+ public function create(User $user): bool
1567
+ {
1568
+ return $user->hasPermissionTo('create posts');
1569
+ }
1570
+
1571
+ public function update(User $user, Post $post): bool
1572
+ {
1573
+ return $user->hasPermissionTo('edit posts') || $post->user_id === $user->id;
1574
+ }
1575
+
1576
+ public function delete(User $user, Post $post): bool
1577
+ {
1578
+ return $user->hasPermissionTo('delete posts');
1579
+ }
1580
+ }
1581
+ ```
1582
+
1583
+ ## Exports & Imports
1584
+
1585
+ ```php
1586
+ // Generate exporter
1587
+ // php artisan make:filament-exporter Customer
1588
+
1589
+ use App\Filament\Exports\CustomerExporter;
1590
+ use Filament\Actions\ExportAction;
1591
+
1592
+ class CustomerExporter extends Exporter
1593
+ {
1594
+ protected static ?string $model = Customer::class;
1595
+
1596
+ public static function getColumns(): array
1597
+ {
1598
+ return [
1599
+ ExportColumn::make('name'),
1600
+ ExportColumn::make('email'),
1601
+ ExportColumn::make('created_at'),
1602
+ ];
1603
+ }
1604
+ }
1605
+
1606
+ // Use in table toolbar
1607
+ ExportAction::make()->exporter(CustomerExporter::class)
1608
+
1609
+ // Generate importer
1610
+ // php artisan make:filament-importer Customer
1611
+
1612
+ use App\Filament\Imports\CustomerImporter;
1613
+ use Filament\Actions\ImportAction;
1614
+
1615
+ class CustomerImporter extends Importer
1616
+ {
1617
+ protected static ?string $model = Customer::class;
1618
+
1619
+ public static function getColumns(): array
1620
+ {
1621
+ return [
1622
+ ImportColumn::make('name')->requiredMapping()->rules(['required', 'max:255']),
1623
+ ImportColumn::make('email')->requiredMapping()->rules(['required', 'email']),
1624
+ ];
1625
+ }
1626
+ }
1627
+
1628
+ ImportAction::make()->importer(CustomerImporter::class)
1629
+ ```
1630
+
1631
+ ## Testing
1632
+
1633
+ ```php
1634
+ use function Pest\Livewire\livewire;
1635
+
1636
+ // Test list page renders
1637
+ it('can render the list page', function () {
1638
+ livewire(ListCustomers::class)->assertSuccessful();
1639
+ });
1640
+
1641
+ // Test table shows records
1642
+ it('can list customers', function () {
1643
+ $customers = Customer::factory()->count(5)->create();
1644
+
1645
+ livewire(ListCustomers::class)
1646
+ ->assertCanSeeTableRecords($customers);
1647
+ });
1648
+
1649
+ // Test table columns exist
1650
+ it('has the expected columns', function () {
1651
+ livewire(ListCustomers::class)
1652
+ ->assertTableColumnExists('name')
1653
+ ->assertTableColumnExists('email')
1654
+ ->assertTableColumnVisible('status');
1655
+ });
1656
+
1657
+ // Test table search
1658
+ it('can search customers by name', function () {
1659
+ $customer = Customer::factory()->create(['name' => 'John Doe']);
1660
+ Customer::factory()->count(5)->create();
1661
+
1662
+ livewire(ListCustomers::class)
1663
+ ->searchTable('John')
1664
+ ->assertCanSeeTableRecords([$customer])
1665
+ ->assertCountTableRecords(1);
1666
+ });
1667
+
1668
+ // Test table filters
1669
+ it('can filter by status', function () {
1670
+ $active = Customer::factory()->create(['status' => 'active']);
1671
+ $inactive = Customer::factory()->create(['status' => 'inactive']);
1672
+
1673
+ livewire(ListCustomers::class)
1674
+ ->filterTable('status', 'active')
1675
+ ->assertCanSeeTableRecords([$active])
1676
+ ->assertCanNotSeeTableRecords([$inactive]);
1677
+ });
1678
+
1679
+ // Test create form
1680
+ it('can create a customer', function () {
1681
+ livewire(CreateCustomer::class)
1682
+ ->fillForm([
1683
+ 'name' => 'Jane Doe',
1684
+ 'email' => 'jane@example.com',
1685
+ 'status' => 'active',
1686
+ ])
1687
+ ->call('create')
1688
+ ->assertHasNoFormErrors();
1689
+
1690
+ assertDatabaseHas('customers', ['email' => 'jane@example.com']);
1691
+ });
1692
+
1693
+ // Test form validation
1694
+ it('validates required fields', function () {
1695
+ livewire(CreateCustomer::class)
1696
+ ->fillForm(['name' => ''])
1697
+ ->call('create')
1698
+ ->assertHasFormErrors(['name' => 'required']);
1699
+ });
1700
+
1701
+ // Test edit form
1702
+ it('can update a customer', function () {
1703
+ $customer = Customer::factory()->create();
1704
+
1705
+ livewire(EditCustomer::class, ['record' => $customer->getRouteKey()])
1706
+ ->fillForm(['name' => 'Updated Name'])
1707
+ ->call('save')
1708
+ ->assertHasNoFormErrors();
1709
+
1710
+ expect($customer->fresh()->name)->toBe('Updated Name');
1711
+ });
1712
+
1713
+ // Test table actions
1714
+ it('can delete a customer', function () {
1715
+ $customer = Customer::factory()->create();
1716
+
1717
+ livewire(ListCustomers::class)
1718
+ ->callTableAction('delete', $customer);
1719
+
1720
+ assertDatabaseMissing('customers', ['id' => $customer->id]);
1721
+ });
1722
+
1723
+ // Test bulk actions
1724
+ it('can bulk delete customers', function () {
1725
+ $customers = Customer::factory()->count(3)->create();
1726
+
1727
+ livewire(ListCustomers::class)
1728
+ ->callTableBulkAction('delete', $customers);
1729
+
1730
+ assertDatabaseCount('customers', 0);
1731
+ });
1732
+ ```
1733
+
1734
+ ## Common Patterns
1735
+
1736
+ ### Global Configuration (Service Provider)
1737
+
1738
+ ```php
1739
+ use Illuminate\Support\ServiceProvider;
1740
+ use Filament\Tables\Table;
1741
+ use Filament\Tables\Columns\TextColumn;
1742
+ use Filament\Schemas\Components\Section;
1743
+
1744
+ class AppServiceProvider extends ServiceProvider
1745
+ {
1746
+ public function boot(): void
1747
+ {
1748
+ // Apply columns to every table
1749
+ Table::configureUsing(function (Table $table) {
1750
+ $table->pushColumns([
1751
+ TextColumn::make('created_at')
1752
+ ->dateTime()
1753
+ ->sortable()
1754
+ ->toggleable(isToggledHiddenByDefault: true),
1755
+ TextColumn::make('updated_at')
1756
+ ->dateTime()
1757
+ ->sortable()
1758
+ ->toggleable(isToggledHiddenByDefault: true),
1759
+ ]);
1760
+ });
1761
+
1762
+ // Apply to all sections
1763
+ Section::configureUsing(fn (Section $section) => $section->columns(2));
1764
+ }
1765
+ }
1766
+ ```
1767
+
1768
+ ### Soft Deletes in Resource
1769
+
1770
+ ```php
1771
+ // In table class
1772
+ use Filament\Tables\Filters\TrashedFilter;
1773
+
1774
+ $table->filters([TrashedFilter::make()])
1775
+
1776
+ // In resource
1777
+ use Illuminate\Database\Eloquent\SoftDeletingScope;
1778
+
1779
+ public static function getEloquentQuery(): Builder
1780
+ {
1781
+ return parent::getEloquentQuery()
1782
+ ->withoutGlobalScopes([SoftDeletingScope::class]);
1783
+ }
1784
+ ```
1785
+
1786
+ ### Simple (Modal) Resources
1787
+
1788
+ ```php
1789
+ // Generated with --simple flag
1790
+ // Uses modals instead of separate create/edit pages
1791
+ class CategoryResource extends Resource
1792
+ {
1793
+ protected static ?string $model = Category::class;
1794
+
1795
+ public static function form(Schema $schema): Schema { /* ... */ }
1796
+ public static function table(Table $table): Table { /* ... */ }
1797
+
1798
+ public static function getPages(): array
1799
+ {
1800
+ return [
1801
+ 'index' => ListCategories::route('/'),
1802
+ ];
1803
+ }
1804
+ }
1805
+ ```
1806
+
1807
+ ### Multiple Panels
1808
+
1809
+ ```php
1810
+ // Second panel provider (e.g., for customers)
1811
+ class CustomerPanelProvider extends PanelProvider
1812
+ {
1813
+ public function panel(Panel $panel): Panel
1814
+ {
1815
+ return $panel
1816
+ ->id('customer')
1817
+ ->path('customer')
1818
+ ->login()
1819
+ ->colors(['primary' => Color::Blue])
1820
+ ->discoverResources(
1821
+ in: app_path('Filament/Customer/Resources'),
1822
+ for: 'App\\Filament\\Customer\\Resources'
1823
+ );
1824
+ }
1825
+ }
1826
+ ```
1827
+
1828
+ ## Gotchas
1829
+
1830
+ 1. **v5 file structure**: Resources split forms/tables into separate classes under `Schemas/` and `Tables/` directories.
1831
+ 2. **`live()` is required** for reactive fields -- without it, `afterStateUpdated` won't fire.
1832
+ 3. **Dot notation** auto-eager-loads relationships: `TextColumn::make('author.name')`.
1833
+ 4. **Authorization** uses model policies by default -- if pages 404 or actions are hidden, check your policy methods.
1834
+ 5. **`configureUsing()`** in service providers applies globally to all instances of a component type.
1835
+ 6. **Utility injection** uses named parameters: `$state`, `$get`, `$set`, `$record`, `$operation`. Don't use positional args.
1836
+ 7. **JS reactivity** (`hiddenJs`, `actionJs`) runs entirely client-side -- use for simple toggle/show/hide logic without server calls.
1837
+ 8. **`ignoreRecord: true`** on unique validation is required for edit forms, otherwise the current record fails its own uniqueness check.
1838
+ 9. **`dehydrated(false)`** prevents a field's value from being saved to the database -- use for display-only calculated fields.
1839
+ 10. **Relation managers** vs **Select with relationship**: Use relation managers for HasMany/BelongsToMany management with full CRUD. Use `Select::make()->relationship()` for BelongsTo assignments.
1840
+ 11. **Navigation badges** returning `null` hides the badge; return `'0'` (string) to show zero.
1841
+ 12. **`->searchable()` on columns** adds per-column search. `->searchable(isGlobal: true)` adds to global search. Resource-level `$recordTitleAttribute` also feeds global search.