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

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

3 min read Mohamed Said Mohamed Said

Why Filament v4 Rethinks the Component Tree

Filament v3 kept forms and infolists as parallel but separate hierarchies. You defined form(Form $form) and infolist(Infolist $infolist) independently, duplicating field definitions whenever you wanted a read-only view alongside an editable one. Filament v4 collapses this into a unified Schema API: one component tree that can render in both contexts.

This is not a cosmetic change. It affects how you structure resources, build reusable field sets, and test panel behaviour.


The Schema Entry Point

In v4, Resource classes expose a schema() method that returns a Schema instance. Both the form and the infolist delegate to it:

use Filament\Schemas\Schema;
use Filament\Forms\Components\TextInput;
use Filament\Forms\Components\DatePicker;
use Filament\Infolists\Components\TextEntry;

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

        DatePicker::make('published_at')
            ->nullable(),
    ]);
}

Filament resolves whether it is rendering a form or an infolist and maps components accordingly. TextInput in a form context becomes a TextEntry in an infolist context unless you override it explicitly.


Explicit Infolist Overrides

Automatic mapping covers the common cases, but you will sometimes need a richer read-only presentation. Use ->infolistComponent() on any schema component to swap it out:

use Filament\Infolists\Components\ImageEntry;

TextInput::make('avatar_url')
    ->label('Avatar URL')
    ->url()
    ->infolistComponent(
        ImageEntry::make('avatar_url')->circular()
    ),

The form still renders a URL input; the infolist renders a circular image. One definition, two presentations.


Extracting Reusable Schema Fragments

The real productivity gain is extracting shared fragments into plain classes:

namespace App\Filament\Schemas;

use Filament\Forms\Components\Section;
use Filament\Forms\Components\TextInput;
use Filament\Forms\Components\Textarea;

final class AuthorSchema
{
    public static function components(): array
    {
        return [
            Section::make('Author')
                ->columns(2)
                ->schema([
                    TextInput::make('author_name')->required(),
                    Textarea::make('author_bio')->columnSpanFull(),
                ]),
        ];
    }
}

Then spread it into any resource schema:

return $schema->components([
    ...AuthorSchema::components(),
    TextInput::make('title')->required(),
]);

No trait magic, no inheritance — just plain PHP arrays.


Conditional Visibility Without Duplication

Schema components carry ->visibleOn() and ->hiddenOn() helpers that accept context strings ('form', 'infolist', or custom panel identifiers):

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

TextEntry::make('audit_log')
    ->visibleOn('infolist'),

This lets you keep a single schema while still surfacing fields that only make sense in one context.


Testing the Unified Schema

Pest assertions work against the resolved context. Use livewire() to target the specific page class:

use App\Filament\Resources\PostResource\Pages\EditPost;

it('validates required fields on edit', function () {
    $post = Post::factory()->create();

    livewire(EditPost::class, ['record' => $post->getRouteKey()])
        ->fillForm(['title' => ''])
        ->call('save')
        ->assertHasFormErrors(['title' => 'required']);
});

For infolist assertions, target the ViewPost page and assert entries are visible:

use App\Filament\Resources\PostResource\Pages\ViewPost;

it('shows author name in infolist', function () {
    $post = Post::factory()->create(['author_name' => 'Ada Lovelace']);

    livewire(ViewPost::class, ['record' => $post->getRouteKey()])
        ->assertSeeText('Ada Lovelace');
});

Key Takeaways

  • One schema, two contexts: v4's unified Schema API eliminates the form/infolist duplication that plagued v3 resources.
  • Automatic component mapping handles the common cases; ->infolistComponent() handles the rest.
  • Fragment classes are the idiomatic way to share schema sections across resources — no traits needed.
  • ->visibleOn() / ->hiddenOn() give per-context visibility without splitting the schema.
  • Pest tests target page classes directly; form and infolist assertions remain distinct even though the schema is shared.

Found this useful?

Frequently Asked Questions

3 questions
Q01 Does the unified Schema API mean I can no longer customise the infolist independently?
No. You can still override individual components with `->infolistComponent()` and control visibility per context with `->visibleOn()` / `->hiddenOn()`. The unified API reduces duplication for the common case while preserving full control when you need it.
Q02 Are Filament v3 form and infolist definitions still valid in v4?
Filament v4 ships compatibility shims for the separate `form()` and `infolist()` methods, but they are deprecated. The recommended migration path is to consolidate into a single `schema()` method and use context helpers for any divergence.
Q03 How do I share a schema fragment between a CreatePost and EditPost page?
Extract the shared components into a static method on a plain PHP class (e.g. `PostSchema::components()`) and spread the array into each resource's `schema()` call. No base class or trait is required.

Continue reading

More Articles

View all