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

3 min read Mohamed Said Mohamed Said

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:

  1. Upgrade composer depsfilament/filament:^4.0 with --no-scripts first.
  2. Run the upgrade commandphp artisan filament:upgrade patches the most mechanical changes (import paths, method renames).
  3. Fix schema methods — convert form() / infolist() to schema() resource by resource.
  4. Audit custom fields — replace getFormComponent() with render().
  5. Review action closures — add explicit type hints; test confirmation dialogs.
  6. 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:upgrade handles ~60% of mechanical renames — always run it first.
  • Custom field plugins must replace getFormComponent() with ComponentRenderer::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.

Found this useful?

Frequently Asked Questions

3 questions
Q01 Can I migrate resources one at a time without breaking the whole panel?
Yes. Filament v4 ships a compatibility shim for the v3 `form()` and `infolist()` signatures that emits a deprecation notice but does not throw. Migrate resource by resource, running your Pest suite after each one.
Q02 Do Filament v3 custom field packages work in v4 without changes?
Not without a small update. Any package that implements `getFormComponent()` must replace it with `render(): ComponentRenderer`. The view itself is usually unchanged; only the method signature and return type differ.
Q03 Are Livewire-based Filament tests affected by the v4 upgrade?
The test helper API (`livewire(ResourcePage::class)->fillForm()->assertFormSet()`) is stable across v3 and v4. Your existing Pest feature tests should pass without modification after migrating a resource's schema.

Continue reading

More Articles

View all