Skip to content
laravel-nova-examples.com

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 view page and an infolist() if you need a read-only detail view.
  • Action log. Nova's Actionable trait records action runs. In Filament you need your own audit logging, for example with an activity log package.

Reference