Convert a Nova resource to a Filament resource
A side-by-side mapping of a Laravel Nova 5 resource to a Filament 4 resource, covering fields, table columns, filters, actions and authorization.
- Nova
- 5.x
- Published
- Reading time
- 5 min
Nova and Filament both build admin panels from PHP classes, but they split the work differently. A Nova resource has one fields() method that drives the index, detail and form views. A Filament resource separates the form (create and edit) from the table (index), and each has its own components.
This guide converts the same Order resource from Nova 5 to Filament 4.
The Nova resource
<?php namespace App\Nova; use Laravel\Nova\Fields\BelongsTo; use Laravel\Nova\Fields\Currency; use Laravel\Nova\Fields\DateTime; use Laravel\Nova\Fields\ID; use Laravel\Nova\Fields\Select; use Laravel\Nova\Fields\Text; use Laravel\Nova\Http\Requests\NovaRequest; class Order extends Resource { public static $model = \App\Models\Order::class; public static $title = 'number'; public static $search = ['id', 'number']; public function fields(NovaRequest $request): array { return [ ID::make()->sortable(), Text::make('Number')->sortable()->rules('required', 'max:32'), BelongsTo::make('Customer')->searchable(), Select::make('Status')->options([ 'pending' => 'Pending', 'paid' => 'Paid', 'shipped' => 'Shipped', ])->displayUsingLabels()->filterable(), Currency::make('Total')->currency('EUR')->sortable(), DateTime::make('Created At')->exceptOnForms()->sortable(), ]; } public function actions(NovaRequest $request): array { return [ Actions\MarkAsShipped::make(), ]; } }
The equivalent Filament resource
php artisan make:filament-resource Order
<?php namespace App\Filament\Resources\Orders; use App\Filament\Resources\Orders\Pages\CreateOrder; use App\Filament\Resources\Orders\Pages\EditOrder; use App\Filament\Resources\Orders\Pages\ListOrders; use App\Models\Order; use Filament\Actions\EditAction; use Filament\Forms\Components\Select; use Filament\Forms\Components\TextInput; use Filament\Resources\Resource; use Filament\Schemas\Schema; use Filament\Tables\Columns\TextColumn; use Filament\Tables\Filters\SelectFilter; use Filament\Tables\Table; class OrderResource extends Resource { protected static ?string $model = Order::class; protected static ?string $recordTitleAttribute = 'number'; public const STATUSES = [ 'pending' => 'Pending', 'paid' => 'Paid', 'shipped' => 'Shipped', ]; public static function form(Schema $schema): Schema { return $schema->components([ TextInput::make('number')->required()->maxLength(32), Select::make('customer_id') ->relationship('customer', 'name') ->searchable() ->required(), Select::make('status')->options(self::STATUSES)->required(), TextInput::make('total')->numeric()->prefix('€'), ]); } public static function table(Table $table): Table { return $table ->columns([ TextColumn::make('id')->sortable(), TextColumn::make('number')->sortable()->searchable(), TextColumn::make('customer.name'), TextColumn::make('status')->formatStateUsing(fn (string $state) => self::STATUSES[$state] ?? $state), TextColumn::make('total')->money('EUR')->sortable(), TextColumn::make('created_at')->dateTime()->sortable(), ]) ->filters([ SelectFilter::make('status')->options(self::STATUSES), ]) ->recordActions([ EditAction::make(), ]); } public static function getPages(): array { return [ 'index' => ListOrders::route('/'), 'create' => CreateOrder::route('/create'), 'edit' => EditOrder::route('/{record}/edit'), ]; } }
Mapping table
| Nova | Filament |
|---|---|
public static $model |
protected static ?string $model |
public static $title |
protected static ?string $recordTitleAttribute |
public static $search |
->searchable() on each table column that should be searched |
fields() on forms |
form(Schema $schema) components |
fields() on index |
table(Table $table) columns |
->rules('required', 'max:32') |
->required()->maxLength(32) |
BelongsTo::make('Customer') |
Select::make('customer_id')->relationship('customer', 'name') in the form, TextColumn::make('customer.name') in the table |
->exceptOnForms() |
Leave the field out of form() |
->filterable() or a filter class |
SelectFilter in ->filters() |
| Policies | Policies (Filament uses the same Laravel model policies) |
Converting the action
The Nova action from the action example becomes a table action. Its fields() become the action's schema. Both authorization checks move into visible(): the permission from canSee and the per-record status check from canRun. Filament refuses to run an action that is not visible for the record, so the check also protects the server side:
Action::make('markAsShipped') ->label('Mark as shipped') ->requiresConfirmation() ->schema([ TextInput::make('tracking_number')->required()->maxLength(64), ]) ->visible(fn (Order $record): bool => $record->status === 'paid' && auth()->user()->can('ship', Order::class)) ->action(function (Order $record, array $data): void { $record->forceFill([ 'status' => 'shipped', 'tracking_number' => $data['tracking_number'], 'shipped_at' => now(), ])->save(); }),
Add it to ->recordActions([...]) next to EditAction::make() and import Filament\Actions\Action. For the bulk variant that acts on several selected orders, use BulkAction in ->toolbarActions().
What has no direct equivalent
- Lenses. Filament has no lens concept. Use a separate resource page with its own table query, or a table filter that switches the query.
- Detail views. Nova shows a detail page by default. In Filament, add a
viewpage and aninfolist()if you need a read-only detail view. - Action log. Nova's
Actionabletrait records action runs. In Filament you need your own audit logging, for example with an activity log package.