Why v4 Is Not a Cosmetic Upgrade
Filament v4 ships a unified Schema API that collapses the previously separate form and infolist component trees into a single composable layer. If your codebase leans heavily on custom field classes, render hooks, or action closures, you will feel every one of those changes. This article focuses on the concrete diff — what breaks, why, and how to fix it.
1. Panel Provider Bootstrap Changes
v3 registered panels inside AppServiceProvider or a dedicated PanelProvider that extended PanelProvider directly.
v4 requires every panel class to implement HasForms, HasTables, and HasActions via the new InteractsWithForms concern at the panel level, not just on Livewire components.
// v3
class AdminPanelProvider extends PanelProvider
{
public function panel(Panel $panel): Panel
{
return $panel
->default()
->id('admin')
->path('admin')
->resources([UserResource::class]);
}
}
// v4 — note the explicit ->spa() and ->unsavedChangesAlerts() moves
class AdminPanelProvider extends PanelProvider
{
public function panel(Panel $panel): Panel
{
return $panel
->default()
->id('admin')
->path('admin')
->spa() // moved from plugin config
->unsavedChangesAlerts() // moved from config
->resources([UserResource::class]);
}
}
The ->spa() and ->unsavedChangesAlerts() calls were previously buried in config/filament.php. They are now first-class panel fluent methods.
2. Schema API: Forms and Infolists Unified
The biggest conceptual shift. In v3, Forms\Components\* and Infolists\Components\* were parallel but separate namespaces. In v4 both resolve through Filament\Schemas\Components\*.
// v3 form schema
use Filament\Forms\Components\TextInput;
use Filament\Forms\Components\Select;
public static function form(Form $form): Form
{
return $form->schema([
TextInput::make('name')->required(),
Select::make('role')->options(Role::class),
]);
}
// v4 — same components, new namespace, Form still accepted
use Filament\Schemas\Components\TextInput;
use Filament\Schemas\Components\Select;
public static function form(Form $form): Form
{
return $form->schema([
TextInput::make('name')->required(),
Select::make('role')->options(Role::class),
]);
}
The Form and Infolist wrapper objects remain, but they now both accept Schema components. A Rector rule ships with v4 to automate the namespace rewrite — run it first:
vendor/bin/rector process app --config vendor/filament/filament/rector.php
3. Action Closure Signatures
v3 actions injected the record via a $record parameter resolved by name. v4 uses typed injection exclusively.
// v3
Action::make('approve')
->action(function ($record, array $data): void {
$record->approve($data['note']);
});
// v4 — type-hint required; $data still works as named param
Action::make('approve')
->action(function (Post $record, array $data): void {
$record->approve($data['note']);
});
Untyped $record parameters now throw a BindingResolutionException at runtime. The fix is mechanical but must be applied across every resource, relation manager, and custom page.
4. Table Column Extractions
TextColumn::make() no longer accepts raw HTML via ->html() by default — it must be explicitly opted in and sanitised:
// v4
TextColumn::make('bio')
->html()
->sanitizeHtml(); // new — strips disallowed tags via HTMLPurifier
Omitting ->sanitizeHtml() when ->html() is set triggers a deprecation warning in v4 and will become an exception in v4.x.
5. Testing After Migration
Filament's Pest helpers are largely unchanged, but the component class paths in livewire() calls must reflect the new panel structure:
it('can approve a post', function () {
$post = Post::factory()->create();
livewire(PostResource\Pages\EditPost::class, ['record' => $post->getRouteKey()])
->callAction('approve', data: ['note' => 'Looks good'])
->assertHasNoActionErrors();
expect($post->fresh()->status)->toBe(PostStatus::Approved);
});
No changes needed here — the Pest helpers abstract the internal wiring.
Key Takeaways
- Run the bundled Rector config first; it handles ~70% of namespace rewrites automatically.
- Type-hint every action closure's
$recordparameter — untyped injection is gone. ->spa()and->unsavedChangesAlerts()move into the panel fluent chain.TextColumn::html()now requires an explicit->sanitizeHtml()opt-in.- Pest-based Filament tests need minimal changes; focus migration effort on resource and action PHP files.