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

Filament v3 to v4 Migration: Breaking Changes and Practical Refactor Patterns

4 min read Mohamed Said Mohamed Said

Why v4 Is Not a Cosmetic Upgrade

Filament v4 ships a unified Schema API that replaces the parallel Form / Infolist component trees from v3. If you have large resources with custom fields, repeated make() factories, or inline closures scattered across form() and infolist() methods, you will touch almost every resource file. The good news: the migration is mechanical once you understand the three core shifts.


Breaking Change 1 — Schema Replaces Parallel Component Trees

In v3 you maintained two separate component hierarchies:

// 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 methods receive a Schema object and share the same component namespace when you opt into the unified path:

// v4
use Filament\Schema\Schema;

public static function form(Schema $schema): Schema
{
    return $schema->components([
        TextInput::make('name')->required(),
    ]);
}

public static function infolist(Schema $schema): Schema
{
    return $schema->components([
        TextEntry::make('name'),
    ]);
}

The ->schema() method still works as an alias during the transition period, but relying on it will generate deprecation notices and will be removed in a future minor.


Breaking Change 2 — Action mountUsing and fillForm Signatures

v3 actions used mountUsing to pre-fill a modal form:

// v3
Action::make('approve')
    ->mountUsing(fn (ComponentContainer $form, Model $record) =>
        $form->fill(['note' => $record->last_note])
    )

v4 replaces ComponentContainer with Schema and renames the hook:

// v4
use Filament\Schema\Schema;

Action::make('approve')
    ->fillForm(fn (Model $record): array => [
        'note' => $record->last_note,
    ])
    ->form([
        Textarea::make('note')->required(),
    ])

mountUsing is removed entirely. Any resource or page that injects ComponentContainer will throw a class-not-found error at runtime — grep your codebase before upgrading.


Breaking Change 3 — Removed ->columns() Shorthand on Groups

v3 allowed Grid::make()->columns(2) as a fluent shorthand. v4 enforces Grid::make(2) as the canonical constructor argument:

// v3 — still parses but deprecated
Grid::make()->columns(2)

// v4 — correct
Grid::make(2)->schema([...])

This is a silent runtime regression in v3 compatibility mode: the grid renders as a single column without an error. Add a search for ->columns( on Grid instances to your pre-upgrade checklist.


Refactor Pattern: Extract a Shared Schema Method

When form and infolist share 80 % of their fields, extract a private static method rather than duplicating:

private static function coreFields(bool $readonly = false): array
{
    return [
        TextInput::make('name')
            ->required()
            ->disabled($readonly),
        DatePicker::make('published_at')
            ->disabled($readonly),
    ];
}

public static function form(Schema $schema): Schema
{
    return $schema->components(static::coreFields());
}

public static function infolist(Schema $schema): Schema
{
    return $schema->components(static::coreFields(readonly: true));
}

This pattern eliminates drift between the two views and makes future field additions a single-line change.


Updating Pest Tests

Filament's test helpers mirror the API changes. The assertFormFieldExists and assertInfolists assertions now accept a Schema context:

it('renders the name field in the form', function () {
    livewire(EditPost::class, ['record' => Post::factory()->create()])
        ->assertFormFieldExists('name')
        ->assertFormFieldIsRequired('name');
});

No change needed here — the helpers are backward-compatible. What does break is any test that directly instantiates ComponentContainer:

// Remove this pattern entirely
$container = ComponentContainer::make($livewire)->statePath('data');

Replace with the Livewire component test helpers; they handle schema resolution internally.


Takeaways

  • Grep for ComponentContainer, mountUsing, and ->columns( on Grid before touching anything else.
  • The Schema type hint is the single biggest mechanical change — a project-wide find-and-replace handles 90 % of it.
  • Extract shared field arrays into private static methods to avoid form/infolist drift.
  • fillForm(fn) replaces mountUsing for action pre-population; the old hook is gone, not deprecated.
  • Pest test helpers are largely compatible; only direct ComponentContainer instantiation breaks.

Found this useful?

Frequently Asked Questions

3 questions
Q01 Can I upgrade a large Filament v3 app to v4 incrementally?
Filament v4 ships a compatibility layer that keeps `->schema()` as an alias and tolerates some v3 patterns, but `ComponentContainer` and `mountUsing` are hard-removed. You must fix those before the app boots. Everything else can be migrated resource by resource.
Q02 Do custom Filament v3 field plugins need to be rewritten for v4?
Plugins that extend `Field` and only override `getView()` or `setUp()` typically need only a type-hint update from `ComponentContainer` to `Schema`. Plugins that hook into the component tree lifecycle more deeply will need targeted refactoring around the new Schema render pipeline.
Q03 Will Filament v3 receive security patches after v4 is stable?
The Filament team has historically maintained the previous major for critical security fixes for a limited window after a new major ships. Check the official GitHub releases page for the current support policy rather than relying on community estimates.

Continue reading

More Articles

View all