Filament v3 to v4: Breaking Changes, Migration Patterns, and Refactor Strategies
#filament #laravel #upgrade #filament-v4

Filament v3 to v4: Breaking Changes, Migration Patterns, and Refactor Strategies

4 min read Mohamed Said Mohamed Said

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) with Grid::make()->columns(int) across all resources.
  • Swap ->state() on infolist entries for ->getStateUsing().
  • Audit action closures for positional $record assumptions; 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 ComponentContainer references—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:upgrade as the first step, not the last.

Found this useful?

Frequently Asked Questions

3 questions
Q01 Is Filament v4 backwards compatible with v3 custom field plugins?
Not fully. Plugins that reference `ComponentContainer` directly will break because the class was renamed in v4. You need to update the import and any method calls that relied on the old container API before the plugin will render correctly.
Q02 Do I need to republish all Filament assets after upgrading to v4?
Yes. Run `php artisan filament:upgrade` followed by `php artisan filament:assets` to ensure compiled JS and CSS assets match the v4 component tree. Stale v3 assets cause subtle UI regressions that are hard to trace.
Q03 Will my v3 Pest tests pass after upgrading to v4?
Tests that assert on top-level form fields often pass, but any test using `assertFormFieldExists` on a field nested inside a Section or Grid will fail because v4 requires the full dot-notation schema path. Audit and update those assertions as part of the migration.

Continue reading

More Articles

View all