Filament v4 Migrating from v3: Breaking Changes and Refactor Patterns
#filament #laravel #migration #filament-v4

Filament v4 Migrating from v3: Breaking Changes and Refactor Patterns

4 min read Mohamed Said Mohamed Said

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 $infolist signatures become Schema $schema — update every resource.
  • ->schema([]) on layout components becomes ->components([]) — automate with a careful find-replace.
  • ->successNotificationTitle() and ->failureNotificationTitle() are removed; use ->successNotification() with a Notification object.
  • Separate state resolution (->state()) from formatting (->formatStateUsing()) in table columns.
  • Extract SchemaBuilder classes for large resources to isolate your domain logic from Filament's API surface.

Found this useful?

Frequently Asked Questions

3 questions
Q01 Can I migrate resources one at a time, or does v4 require all-at-once?
Filament v4 is a hard dependency upgrade, so all resources must be compatible before you can run the application. However, you can batch the mechanical changes (schema/components rename) with a script and then address the action/notification API changes resource by resource.
Q02 Does the `->schema()` method still exist anywhere in v4?
Yes — the root `Schema` object passed into `form()` and `infolist()` still exposes `->components()` as the primary method. The `->schema()` alias was removed from layout components like Section and Grid, which is where the confusion arises.
Q03 Are custom Filament v3 field plugins compatible with v4 out of the box?
Usually not without changes. Plugins that extend `Field` or `Column` and call `->schema()` internally need updating. Check the plugin's GitHub issues or changelog before upgrading, and pin the plugin version until the maintainer ships a v4-compatible release.

Continue reading

More Articles

View all