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

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

3 min read Mohamed Said Mohamed Said

Why the Old Approach Felt Redundant

In Filament v3, a typical resource forced you to define form() and infolist() separately. If your OrderResource displayed twelve fields, you wrote those fields twice — once as TextInput components, once as TextEntry components. The logic was identical; only the component class differed. Senior engineers reached for shared methods or traits to paper over the duplication, but it was always a workaround.

Filament v4 addresses this at the framework level with the unified Schema API.

The Unified Schema API in Practice

The core idea: a Schema is a context-aware tree of components. The same schema definition renders as an editable form inside CreateRecord or EditRecord, and as a read-only infolist inside ViewRecord. Components resolve their own rendering based on the active context.

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

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

                Select::make('status')
                    ->options(OrderStatus::class)
                    ->required(),
            ]),
    ]);
}

You define schema() once on the resource. Filament resolves TextInput to an editable input on the form page and to a read-only text entry on the view page — no duplication, no trait gymnastics.

Opting Into Context-Specific Behaviour

Sometimes a field genuinely needs different behaviour per context. The API provides ->visibleOn() and ->hiddenOn() helpers, plus the lower-level ->when() callback that receives the active context string ('form', 'infolist'):

TextInput::make('internal_notes')
    ->hiddenOn('infolist'),

Select::make('assigned_to')
    ->options(fn () => User::pluck('name', 'id'))
    ->when(
        fn (string $context) => $context === 'form',
        fn (Select $field) => $field->searchable()->preload()
    ),

This keeps the single-schema principle intact while giving you an escape hatch.

Reusable Schema Fragments

Because a schema is just a PHP value, you can extract fragments into dedicated classes and compose them:

class AddressSchema
{
    public static function make(): array
    {
        return [
            TextInput::make('street')->required(),
            TextInput::make('city')->required(),
            TextInput::make('postcode')->required(),
        ];
    }
}

// Inside your resource:
Section::make('Shipping Address')
    ->schema(AddressSchema::make()),

This is the pattern that replaces the old getFormSchema() / getInfolistSchema() split. One fragment, used everywhere.

Table Columns Are Still Separate

It is worth being explicit: the unified Schema API covers forms and infolists only. Table column definitions remain in table() and have not been merged. This is intentional — table columns carry sorting, searching, and bulk-action concerns that do not map cleanly onto a field/entry duality.

Testing the New Schema

Pest assertions against Filament v4 schemas use the same livewire() helper, but the component class changes:

use function Pest\Livewire\livewire;

it('saves an order', function () {
    livewire(\App\Filament\Resources\OrderResource\Pages\CreateOrder::class)
        ->fillForm([
            'name' => 'Acme Corp',
            'status' => 'pending',
        ])
        ->call('create')
        ->assertHasNoFormErrors();
});

The assertion surface is unchanged; only the underlying schema resolution differs.

Key Takeaways

  • One schema() method replaces separate form() and infolist() definitions on a resource.
  • Components are context-aware — they render as inputs or entries depending on the active page.
  • Use ->when() or ->hiddenOn() for the rare cases where context-specific behaviour is genuinely needed.
  • Extract reusable fragments into plain PHP classes; they compose cleanly into any schema.
  • Table columns remain separate — the unification is scoped to forms and infolists.
  • Existing Pest assertions continue to work; no test-layer rewrite is required.

Found this useful?

Frequently Asked Questions

3 questions
Q01 Do I have to rewrite all my v3 resources to use the unified Schema API when upgrading to v4?
Not immediately. Filament v4 provides compatibility shims so existing `form()` and `infolist()` definitions continue to work during migration. You can migrate resources incrementally, replacing both methods with a single `schema()` method at your own pace.
Q02 Can a single schema component render completely differently in form vs infolist context?
Yes. Each component resolves its own view based on the active context string. You can also use the `->when()` callback to apply arbitrary configuration — different options, validation rules, or visibility — depending on whether the schema is rendering as a form or an infolist.
Q03 Does the unified Schema API affect how Filament handles validation?
Validation rules are only evaluated in form context. When the same schema renders as an infolist, validation is skipped entirely. You do not need to guard your `->required()` or `->rules()` calls — Filament handles the context check internally.

Continue reading

More Articles

View all