Filament v4 Schema-Based Forms: Unified Schema API in Practice
#filament #laravel #filament-v4 #schema-api

Filament v4 Schema-Based Forms: Unified Schema API in Practice

3 min read Mohamed Said Mohamed Said

Filament v4 Schema-Based Forms: Unified Schema API in Practice

Filament v4 introduced one of its most architecturally significant changes: a unified Schema API that replaces the previously separate form() and infolist() definition styles with a single, composable schema() surface. If you have a non-trivial Filament v3 codebase, understanding this shift before you migrate will save you hours of confusion.

What Changed and Why It Matters

In Filament v3, forms and infolists lived in parallel universes. You defined form(Form $form) with Components\TextInput, and separately infolist(Infolist $infolist) with Entries\TextEntry. The duplication was real — the same field often had a twin entry just to display it read-only.

Filament v4 collapses this into a single schema(Schema $schema) method. Components are now context-aware: a TextInput can render as an editable field inside a form context and as a read-only entry inside an infolist context, driven by the same definition.

use Filament\Schema\Schema;
use Filament\Forms\Components\TextInput;
use Filament\Forms\Components\Select;

public function schema(Schema $schema): Schema
{
    return $schema->components([
        TextInput::make('name')
            ->required()
            ->maxLength(255),

        Select::make('status')
            ->options(Status::class)
            ->native(false),
    ]);
}

The same array powers both the create/edit form and the view infolist. No more maintaining two lists.

Composing Reusable Schema Fragments

The real power emerges when you extract domain-specific schema fragments into dedicated classes. Think of these as DTOs for your UI structure.

namespace App\Filament\Schemas;

use Filament\Forms\Components\Fieldset;
use Filament\Forms\Components\TextInput;
use Filament\Forms\Components\DatePicker;

final class AddressSchema
{
    public static function make(string $prefix = ''): array
    {
        $field = fn(string $name) => $prefix ? "{$prefix}.{$name}" : $name;

        return [
            Fieldset::make('Address')
                ->schema([
                    TextInput::make($field('line1'))->required(),
                    TextInput::make($field('city'))->required(),
                    TextInput::make($field('postcode'))->required(),
                ]),
        ];
    }
}

Now any resource that needs an address block calls AddressSchema::make('billing') — one definition, zero drift between form and infolist.

Context-Specific Overrides

Sometimes a field genuinely needs different behaviour in edit vs. view mode. The Schema API provides ->hiddenOn() and ->visibleOn() helpers, plus the ->formatStateUsing() callback that only fires during infolist rendering.

TextInput::make('api_key')
    ->password()
    ->revealable()
    ->hiddenOn('view'), // hide raw key in infolist

TextEntry::make('api_key')
    ->formatStateUsing(fn($state) => '••••' . substr($state, -4))
    ->visibleOn('view'),

This pattern keeps sensitive fields safe without splitting your schema definition.

Custom Schema Components

Building a custom component is now a single class that extends Component and declares both its form and infolist views.

namespace App\Filament\Components;

use Filament\Forms\Components\Component;

class MoneyInput extends Component
{
    protected string $view = 'filament.components.money-input';

    public static function make(string $name): static
    {
        return app(static::class, ['name' => $name]);
    }
}

The Blade view receives $getState(), $isDisabled(), and the full component API — no separate Entry class required.

Performance Note

Because the Schema API resolves components lazily and shares the same component tree for both read and write contexts, Filament v4 avoids instantiating duplicate component graphs per page load. On resource pages with 30+ fields this is a measurable reduction in object allocations, though the gains are most visible under Octane where those allocations compound across requests.

Key Takeaways

  • One schema() method replaces both form() and infolist() in Filament v4.
  • Extract reusable field groups into static schema fragment classes to eliminate duplication.
  • Use ->visibleOn() / ->hiddenOn() for context-specific rendering without splitting definitions.
  • Custom components now require one class and one view, not a form component plus an infolist entry.
  • The unified tree means fewer object allocations, which matters under long-running runtimes like Octane.

Found this useful?

Frequently Asked Questions

3 questions
Q01 Can I still use the old form() and infolist() methods in Filament v4?
Filament v4 deprecates the separate form() and infolist() methods in favour of the unified schema() API. The old methods may still work during a transition period, but new resources should use schema() to avoid future breaking changes.
Q02 How do I handle fields that should only appear in edit mode, not view mode?
Use ->hiddenOn('view') on the component, or ->visibleOn('create', 'edit'). For display-only variants, define a companion TextEntry with ->visibleOn('view') in the same schema array.
Q03 Do third-party Filament v3 plugins work with the v4 Schema API?
Plugins that extend Form components or Infolist entries need to be updated by their maintainers to implement the unified Component contract. Check the plugin's changelog for v4 compatibility before upgrading.

Continue reading

More Articles

View all