Why v4 Is Not a Drop-In Upgrade
Filament v4 ships a unified Schema API that collapses the previously separate Form, Infolist, and Table component trees into a single composable layer. That architectural shift is the source of most breaking changes. If you treat the upgrade as a find-and-replace exercise you will miss subtle runtime errors that only surface on specific resource actions.
This article walks through the highest-impact changes and the refactor patterns that keep your panels clean.
1. The Schema API: What Actually Changed
In v3, form schemas lived inside form(Form $form) and infolist schemas inside infolist(Infolist $infolist). Both accepted a flat array of components. In v4 both methods accept a Schema object, and components are now schema-aware nodes.
v3 form method:
public static function form(Form $form): Form
{
return $form->schema([
TextInput::make('name')->required(),
Select::make('status')->options(Status::class),
]);
}
v4 equivalent:
public static function form(Form $form): Form
{
return $form->schema([
TextInput::make('name')->required(),
Select::make('status')->options(Status::class),
]);
}
The method signature looks identical for simple cases—but the moment you use layout components the differences appear.
Grid and Section Changes
v3 Grid::make(2) accepted a columns integer as the first argument. v4 moves column configuration to a fluent method:
// v3
Grid::make(2)->schema([...]);
// v4
Grid::make()->columns(2)->schema([...]);
This is a silent failure: v3 code compiles in v4 but Grid::make(2) now creates a grid with the default column count and ignores the integer, because the first parameter is no longer $columns.
2. Infolist Refactors
v3 infolists used TextEntry, ImageEntry, and IconEntry from Filament\Infolists\Components. v4 retains these classes but the state() helper is removed in favour of direct model attribute binding through ->record().
If you were calling ->state(fn ($record) => $record->full_name) you must switch to a computed attribute or a custom ->getStateUsing() closure:
// v3 — removed in v4
TextEntry::make('display_name')
->state(fn (User $record): string => $record->first_name.' '.$record->last_name),
// v4
TextEntry::make('display_name')
->getStateUsing(fn (User $record): string => $record->first_name.' '.$record->last_name),
3. Action Class Signature Changes
Table actions in v3 accepted $record as a typed parameter in closures. v4 enforces named argument injection via the container, which means positional assumptions break:
// v3 — positional, works by convention
Action::make('approve')
->action(function (Order $record, array $data): void {
$record->approve($data['note']);
}),
// v4 — explicit named injection, always safe
Action::make('approve')
->action(function (array $data, Order $record): void {
$record->approve($data['note']);
}),
The order no longer matters in v4 because arguments are resolved by name, but you must ensure your closure parameter names match what Filament injects ($record, $data, $livewire, $component).
4. Updating Pest Tests
Filament's test helpers gained stricter type assertions in v4. The assertFormFieldExists helper now requires the full dot-notation path for nested schema components:
// v3
livewire(EditOrder::class, ['record' => $order->getRouteKey()])
->assertFormFieldExists('status');
// v4 — nested inside a Section
livewire(EditOrder::class, ['record' => $order->getRouteKey()])
->assertFormFieldExists('status', 'form', 'details_section');
Run your full Pest suite immediately after upgrading. Schema path mismatches surface here before they surface in the browser.
Migration Checklist
- Replace
Grid::make(int)withGrid::make()->columns(int)across all resources. - Swap
->state()on infolist entries for->getStateUsing(). - Audit action closures for positional
$recordassumptions; switch to named parameters. - Update Pest assertions that reference nested schema paths.
- Re-publish vendor assets:
php artisan filament:upgrade && php artisan filament:assets. - Check any custom field plugins for
ComponentContainerreferences—this class was renamed in v4.
Takeaways
- The unified Schema API is the architectural core of v4; understand it before touching code.
Grid::make(int)silently misbehaves—it is the most common hidden regression.- Action closure injection is now container-driven; parameter order is irrelevant but names are not.
- Pest test paths for nested components changed; update assertions before declaring the migration done.
- Run
filament:upgradeas the first step, not the last.