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.