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
Schemaclass 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()andrequired()closures receive aGetcallable 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.