Filament v4 Schema-Based Forms, Infolists, and the Unified Schema API
#filament #laravel #filament-v4 #admin-panel

Filament v4 Schema-Based Forms, Infolists, and the Unified Schema API

3 min read Mohamed Said Mohamed Said

Filament v4: One Schema to Rule Them All

Filament v4 introduced a fundamental shift in how you define UI: forms and infolists no longer live in separate, parallel APIs with their own component namespaces. Instead, both are expressed through a unified Schema API. If you have built anything non-trivial in Filament v3, this is the change that will reshape your muscle memory the most.

This article focuses on the practical implications — what the unified schema looks like, how to share components between read and write contexts, and where the sharp edges are.


What Changed at the API Level

In v3, a resource carried two distinct method signatures:

// Filament v3
public static function form(Form $form): Form
{
    return $form->schema([
        Forms\Components\TextInput::make('name'),
    ]);
}

public static function infolist(Infolist $infolist): Infolist
{
    return $infolist->schema([
        Infolists\Components\TextEntry::make('name'),
    ]);
}

In v4, both methods accept a Schema object and draw from the same component pool:

// Filament v4
use Filament\Schemas\Schema;
use Filament\Forms\Components\TextInput;
use Filament\Infolists\Components\TextEntry;

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 class is the common container. Components that are purely presentational (TextEntry, ImageEntry) remain in the Infolists namespace, while interactive components (TextInput, Select) stay in Forms. The container itself, however, is now shared.


Reusable Schema Objects

The real productivity gain is extracting shared schema fragments into plain PHP objects:

namespace App\Filament\Schemas;

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

class AddressSchema
{
    public static function fields(): array
    {
        return [
            TextInput::make('street')->required(),
            TextInput::make('city')->required(),
            Select::make('country')
                ->options(Country::pluck('name', 'code'))
                ->searchable(),
        ];
    }
}

Then compose it anywhere:

public static function form(Schema $schema): Schema
{
    return $schema->components([
        ...AddressSchema::fields(),
        TextInput::make('vat_number'),
    ]);
}

No traits, no abstract base resources — just arrays you spread. This is the pattern that scales.


Conditional Visibility Without Livewire Hacks

Filament v4 tightens the visible() / hidden() API so conditions can reference sibling field state without wiring up custom Livewire properties:

Select::make('billing_type')
    ->options([
        'individual' => 'Individual',
        'company'    => 'Company',
    ]),

TextInput::make('vat_number')
    ->visible(fn (Get $get): bool => $get('billing_type') === 'company')
    ->required(fn (Get $get): bool => $get('billing_type') === 'company'),

The Get callable is injected by the schema engine. No $this->billingType property needed on your Livewire component.


Infolist Components in Read-Only Panels

When you render a view page, Filament v4 passes the same Schema contract but the engine switches to read-only rendering automatically. You can still mix presentational and layout components:

public static function infolist(Schema $schema): Schema
{
    return $schema->components([
        Section::make('Identity')->schema([
            TextEntry::make('name'),
            TextEntry::make('email'),
        ]),
        Section::make('Address')->schema(
            AddressSchema::entries() // read-only variant
        ),
    ]);
}

Keeping ::fields() (interactive) and ::entries() (read-only) as separate static methods on your schema class is the cleanest convention I have found.


Key Takeaways

  • The Schema class is now the single container for both forms and infolists; stop importing two separate container types.
  • Extract reusable schema fragments as plain PHP classes with static array-returning methods — spread them with ....
  • visible() and required() closures receive a Get callable for reactive sibling access without extra Livewire state.
  • Maintain separate ::fields() and ::entries() methods on shared schema objects to keep interactive and read-only components clearly separated.
  • The component namespaces (Forms\Components, Infolists\Components) still exist; only the container is unified.

Found this useful?

Frequently Asked Questions

3 questions
Q01 Can I share a single schema definition for both the form and the infolist in Filament v4?
Not entirely — interactive components like TextInput cannot render in read-only infolist context. The practical pattern is a shared schema class with two static methods: one returning form fields and one returning infolist entries, keeping the layout logic DRY while using the correct component types for each context.
Q02 Does the unified Schema API break existing Filament v3 resources?
Yes. The method signatures change from `Form $form` and `Infolist $infolist` to `Schema $schema`, and the `->schema()` call becomes `->components()`. You will also need to update any imports that reference the old container classes. Running the Filament v4 upgrade command handles most of the mechanical renaming.
Q03 Is the Get callable in visible() closures reactive on every keystroke?
Yes. Filament wires the reactive dependency automatically when you use the Get callable inside visible(), hidden(), required(), or disabled(). The Livewire component re-evaluates those closures whenever the referenced field state changes, so there is no need to add extra reactive properties manually.

Continue reading

More Articles

View all