Why v3→v4 Is Not a Minor Bump
Filament v4 rewrites the form and infolist rendering layer around a unified Schema API. If your panels have custom fields, complex resource forms, or bespoke actions, you will touch almost every resource file. The good news: the changes are systematic, and a disciplined refactor order keeps the panel functional throughout.
1. The Schema API Replaces form() and infolist() Signatures
In v3, form() received a Form instance and infolist() received an Infolist instance as separate contracts:
// v3
public static function form(Form $form): Form
{
return $form->schema([
TextInput::make('name')->required(),
]);
}
public static function infolist(Infolist $infolist): Infolist
{
return $infolist->schema([
TextEntry::make('name'),
]);
}
In v4, both collapse into a single schema() method on the resource, and components are context-aware — the same schema renders as a form or infolist depending on the page:
// v4
public static function schema(): array
{
return [
TextInput::make('name')
->required()
->formOnly(), // visible only in form context
TextEntry::make('name')
->infolistOnly(), // visible only in infolist context
];
}
For fields that behave identically in both contexts you simply omit the context modifier — the framework resolves the correct renderer automatically.
2. Action Signature Changes
v3 table actions accepted a closure receiving the record:
// v3
Tables\Actions\Action::make('approve')
->action(fn (Order $record) => $record->approve())
->requiresConfirmation(),
v4 introduces a typed ActionArguments contract and moves confirmation into a fluent builder chain:
// v4
Tables\Actions\Action::make('approve')
->action(function (Order $record, array $data): void {
$record->approve();
})
->confirm(
title: 'Approve order?',
description: 'This cannot be undone.',
),
The requiresConfirmation() shorthand still exists as an alias, but the explicit confirm() builder gives you translated strings and custom icon support without a modal component.
3. Custom Fields: getFormComponent() Is Gone
v3 custom field plugins exposed getFormComponent() to return a Livewire view. v4 replaces this with a render() method that returns a ComponentRenderer:
// v3
public function getFormComponent(): string
{
return 'my-package::colour-swatch';
}
// v4
public function render(): ComponentRenderer
{
return ComponentRenderer::make(
view: 'my-package::colour-swatch',
data: fn () => ['swatches' => $this->getSwatches()],
);
}
This matters because ComponentRenderer participates in the deferred-loading pipeline, so your custom field gets lazy hydration for free.
4. Repeatable Refactor Order
Do not attempt a big-bang migration. Use this order to keep the panel green at each step:
- Upgrade composer deps —
filament/filament:^4.0with--no-scriptsfirst. - Run the upgrade command —
php artisan filament:upgradepatches the most mechanical changes (import paths, method renames). - Fix schema methods — convert
form()/infolist()toschema()resource by resource. - Audit custom fields — replace
getFormComponent()withrender(). - Review action closures — add explicit type hints; test confirmation dialogs.
- Run Pest suite — Filament's test helpers (
livewire(EditOrder::class)->fillForm([...])->call('save')) are unchanged in v4, so existing feature tests catch regressions immediately.
5. Gotcha: Panel Provider Boot Order
v4 defers panel registration to booted() instead of boot(). If you resolve panel-specific services inside boot() in a custom service provider, those services may not yet be registered:
// v4-safe: use booted()
public function booted(): void
{
FilamentAsset::register([
Js::make('my-plugin', __DIR__ . '/../dist/my-plugin.js'),
]);
}
Key Takeaways
- The unified Schema API is the largest surface-area change; migrate resource by resource, not all at once.
php artisan filament:upgradehandles ~60% of mechanical renames — always run it first.- Custom field plugins must replace
getFormComponent()withComponentRenderer::make(). - Action confirmation moves to the fluent
confirm()builder;requiresConfirmation()still works as an alias. - Existing Pest/Livewire test helpers are compatible — lean on them to validate each migrated resource before moving to the next.