Why v3→v4 Is a Real Migration, Not a Bump
Filament v4 is not a cosmetic release. The team unified forms, infolists, and table columns under a single Schema API, removed a handful of convenience statics, and changed how component state flows through resources. If you have a mid-size panel with 20+ resources, expect a focused but non-trivial refactor.
This article walks through the highest-impact changes with concrete before/after examples.
1. The Schema API Replaces Separate form() and infolist() Builders
In v3, form() returned a Form and infolist() returned an Infolist. In v4 both accept a Schema and share the same component tree.
v3
public static function form(Form $form): Form
{
return $form->schema([
Forms\Components\TextInput::make('name')->required(),
]);
}
public static function infolist(Infolist $infolist): Infolist
{
return $infolist->schema([
Infolists\Components\TextEntry::make('name'),
]);
}
v4
use Filament\Schemas\Schema;
public static function form(Schema $schema): Schema
{
return $schema->components([
Forms\Components\TextInput::make('name')->required(),
]);
}
public static function infolist(Schema $schema): Schema
{
return $schema->components([
Infolists\Components\TextEntry::make('name'),
]);
}
The method signature change is the first thing your IDE will flag. Run a project-wide search for Form $form and Infolist $infolist in resource files.
2. ->schema() → ->components() on Layouts
Every layout component (Grid, Section, Fieldset, Tabs\Tab, etc.) that previously accepted ->schema([...]) now uses ->components([...]).
// v3
Forms\Components\Section::make('Details')
->schema([
Forms\Components\TextInput::make('email'),
]);
// v4
Forms\Components\Section::make('Details')
->components([
Forms\Components\TextInput::make('email'),
]);
This is the most widespread mechanical change. A simple regex replacement handles 90% of it:
# dry-run first
grep -rn '->schema(\[' app/Filament
# replace (macOS sed)
find app/Filament -name '*.php' \
-exec sed -i '' 's/->schema(\[/->components([/g' {} +
Verify manually — ->schema() still exists on the root Schema object itself, so a blanket replace will over-correct.
3. Removed Static Helpers on Action
Several static convenience methods on Action were removed in favour of explicit closures.
// v3 — no longer exists
Action::make('approve')
->requiresConfirmation()
->successNotificationTitle('Approved');
// v4 — use notification() explicitly
Action::make('approve')
->requiresConfirmation()
->successNotification(
Notification::make()->title('Approved')->success()
);
Check the changelog for the full list; ->failureNotificationTitle() and ->successNotificationTitle() are both gone.
4. Table Column ->getStateUsing() Signature Change
Custom columns that used ->getStateUsing(fn ($record) => ...) now receive a typed $state parameter when chained after ->state().
// v4 — explicit state pipeline
TextColumn::make('status_label')
->state(fn (Order $record): string => $record->status->value)
->formatStateUsing(fn (string $state): string => Str::title($state));
Separating state resolution from formatting is cleaner and easier to test in isolation.
5. Refactor Pattern: Extract a SchemaBuilder Class
For large resources, avoid bloating form() and infolist() with inline logic. Extract a dedicated class:
final class OrderSchemaBuilder
{
public static function form(): array
{
return [
TextInput::make('reference')->required(),
Select::make('status')->options(OrderStatus::class),
];
}
public static function infolist(): array
{
return [
TextEntry::make('reference'),
TextEntry::make('status')->badge(),
];
}
}
// In OrderResource
public static function form(Schema $schema): Schema
{
return $schema->components(OrderSchemaBuilder::form());
}
This pattern survives future API shifts because the resource itself becomes a thin adapter.
Key Takeaways
Form $form/Infolist $infolistsignatures becomeSchema $schema— update every resource.->schema([])on layout components becomes->components([])— automate with a careful find-replace.->successNotificationTitle()and->failureNotificationTitle()are removed; use->successNotification()with aNotificationobject.- Separate state resolution (
->state()) from formatting (->formatStateUsing()) in table columns. - Extract
SchemaBuilderclasses for large resources to isolate your domain logic from Filament's API surface.