Filament v3 → v4: Breaking Changes and Practical Refactor Patterns
Filament v4 is not a cosmetic release. The schema-based form and infolist API is a genuine architectural shift, and several method signatures changed in ways that will silently break behaviour if you only run composer update and call it done. This article walks through the highest-impact changes and shows concrete before/after code so you can plan your migration sprint.
1. The Unified Schema API
In v3, form() and infolist() each accepted a flat array of component objects. In v4 both methods receive a Schema instance, and components are registered through it.
v3
public static function form(Form $form): Form
{
return $form->schema([
TextInput::make('name')->required(),
Select::make('status')->options(Status::class),
]);
}
v4
use Filament\Schemas\Schema;
public static function form(Schema $schema): Schema
{
return $schema->components([
TextInput::make('name')->required(),
Select::make('status')->options(Status::class),
]);
}
The Form type hint is gone. If you type-hint Form in a v4 panel, the method simply will not be called — no exception, just a blank form. Run a project-wide search for : Form and : Infolist return types and replace them.
2. Action Class Signatures
Table and page actions in v3 accepted a Closure for most modifiers. v4 enforces typed builder callbacks in several places and drops a handful of magic $record-injected closures in favour of explicit fn (Model $record) signatures.
v3 — implicit injection
Action::make('approve')
->action(function ($record) {
$record->approve();
});
v4 — explicit type
Action::make('approve')
->action(function (Post $record): void {
$record->approve();
});
This is mostly additive, but if you relied on Filament resolving $livewire, $table, or $form from the closure signature, verify each one — some were removed from the resolver map.
3. Column and Field extraAttributes Merge Behaviour
v3 merged extra HTML attributes with a simple array_merge, meaning later calls won. v4 uses a keyed deep-merge. If you called extraAttributes() twice on the same component expecting the second call to win, you now get both merged. Audit any component that chains extraAttributes more than once.
4. Panel Provider Boot Order
v3 registered panel plugins inside register(). v4 moves plugin boot to a dedicated boot() hook on the plugin interface. If you wrote a custom plugin, implement boot(Panel $panel): void or your plugin's side effects (registering pages, widgets) will not fire.
class MyPlugin implements Plugin
{
public function getId(): string { return 'my-plugin'; }
public function register(Panel $panel): void
{
// bind services
}
public function boot(Panel $panel): void
{
// register pages, widgets, nav items
$panel->pages([MyPage::class]);
}
}
5. Infolist Entry Namespace Move
All infolist entry classes moved from Filament\Infolists\Components to Filament\Schemas\Components\Infolists. Your IDE will flag the missing imports, but if you have string-based component references in config files or dynamic resolution, those will fail silently at runtime.
# Quick grep to find stale imports
grep -r 'Filament\\Infolists\\Components' app/ --include='*.php'
Upgrade Checklist
- Replace all
: Form/: Infolistreturn types with: Schema - Change
->schema([])calls on forms to->components([]) - Add explicit model types to action closures
- Audit double
extraAttributes()chains - Implement
boot()on any custom plugins - Update infolist entry namespaces
- Run
php artisan filament:upgrade(the official codemod handles ~60% of the above) - Run your Pest feature suite against a staging panel before deploying
Key Takeaways
- The
SchemaAPI is the single biggest surface-area change — fix type hints first. - Silent failures (blank forms, missing nav items) are more common than exceptions; test visually.
- The official upgrade command is a starting point, not a finish line.
- Custom plugins need a
boot()method or they are effectively broken in v4. - Namespace moves are grep-able; automate the find-and-replace before manual review.