Skip to content

Instantly share code, notes, and snippets.

@marceloxp
Last active October 8, 2025 00:06
Show Gist options
  • Select an option

  • Save marceloxp/035778179b9bca62c486ebf7d41a1644 to your computer and use it in GitHub Desktop.

Select an option

Save marceloxp/035778179b9bca62c486ebf7d41a1644 to your computer and use it in GitHub Desktop.
PHP Filament 4 - Resources - IA
title
Overview

import Aside from "@components/Aside.astro"

Introduction

Resources are static classes that are used to build CRUD interfaces for your Eloquent models. They describe how administrators should be able to interact with data from your app using tables and forms.

Creating a resource

To create a resource for the App\Models\Customer model:

php artisan make:filament-resource Customer

This will create several files in the app/Filament/Resources directory:

.
+-- Customers
|   +-- CustomerResource.php
|   +-- Pages
|   |   +-- CreateCustomer.php
|   |   +-- EditCustomer.php
|   |   +-- ListCustomers.php
|   +-- Schemas
|   |   +-- CustomerForm.php
|   +-- Tables
|   |   +-- CustomersTable.php

Your new resource class lives in CustomerResource.php.

The classes in the Pages directory are used to customize the pages in the app that interact with your resource. They're all full-page Livewire components that you can customize in any way you wish.

The classes in the Schemas directory are used to define the content of the forms and infolists for your resource. The classes in the Tables directory are used to build the table for your resource.

Have you created a resource, but it's not appearing in the navigation menu? If you have a [model policy](#authorization), make sure you return `true` from the `viewAny()` method.

Simple (modal) resources

Sometimes, your models are simple enough that you only want to manage records on one page, using modals to create, edit and delete records. To generate a simple resource with modals:

php artisan make:filament-resource Customer --simple

Your resource will have a "Manage" page, which is a List page with modals added.

Additionally, your simple resource will have no getRelations() method, as relation managers are only displayed on the Edit and View pages, which are not present in simple resources. Everything else is the same.

Automatically generating forms and tables

If you'd like to save time, Filament can automatically generate the form and table for you, based on your model's database columns, using --generate:

php artisan make:filament-resource Customer --generate

Handling soft-deletes

By default, you will not be able to interact with deleted records in the app. If you'd like to add functionality to restore, force-delete and filter trashed records in your resource, use the --soft-deletes flag when generating the resource:

php artisan make:filament-resource Customer --soft-deletes

You can find out more about soft-deleting here.

Generating a View page

By default, only List, Create and Edit pages are generated for your resource. If you'd also like a View page, use the --view flag:

php artisan make:filament-resource Customer --view

Specifying a custom model namespace

By default, Filament will assume that your model exists in the App\Models directory. You can pass a different namespace for the model using the --model-namespace flag:

php artisan make:filament-resource Customer --model-namespace=Custom\\Path\\Models

In this example, the model should exist at Custom\Path\Models\Customer. Please note the double backslashes \\ in the command that are required.

Now when generating the resource, Filament will be able to locate the model and read the database schema.

Generating the model, migration and factory at the same time

If you'd like to save time when scaffolding your resources, Filament can also generate the model, migration and factory for the new resource at the same time using the --model, --migration and --factory flags in any combination:

php artisan make:filament-resource Customer --model --migration --factory

Record titles

A $recordTitleAttribute may be set for your resource, which is the name of the column on your model that can be used to identify it from others.

For example, this could be a blog post's title or a customer's name:

protected static ?string $recordTitleAttribute = 'name';

This is required for features like global search to work.

You may specify the name of an [Eloquent accessor](https://laravel.com/docs/eloquent-mutators#defining-an-accessor) if just one column is inadequate at identifying a record.

Resource forms

Resource classes contain a form() method that is used to build the forms on the Create and Edit pages.

By default, Filament creates a form schema file for you, which is referenced in the form() method. This is to keep your resource class clean and organized, otherwise it can get quite large:

use App\Filament\Resources\Customers\Schemas\CustomerForm;
use Filament\Schemas\Schema;

public static function form(Schema $schema): Schema
{
    return CustomerForm::configure($schema);
}

In the CustomerForm class, you can define the fields and layout of your form:

use Filament\Forms\Components\TextInput;
use Filament\Schemas\Schema;

public static function configure(Schema $schema): Schema
{
    return $schema
        ->components([
            TextInput::make('name')->required(),
            TextInput::make('email')->email()->required(),
            // ...
        ]);
}

The components() method is used to define the structure of your form. It is an array of fields and layout components, in the order they should appear in your form.

Check out the Forms docs for a guide on how to build forms with Filament.

If you would prefer to define the form directly in the resource class, you can do so and delete the form schema class altogether:
```php
use Filament\Forms\Components\TextInput;
use Filament\Schemas\Schema;

public static function form(Schema $schema): Schema
{
    return $schema
        ->components([
            TextInput::make('name')->required(),
            TextInput::make('email')->email()->required(),
            // ...
        ]);
}
```

Hiding components based on the current operation

The hiddenOn() method of form components allows you to dynamically hide fields based on the current page or action.

In this example, we hide the password field on the edit page:

use Filament\Forms\Components\TextInput;
use Filament\Support\Enums\Operation;

TextInput::make('password')
    ->password()
    ->required()
    ->hiddenOn(Operation::Edit),

Alternatively, we have a visibleOn() shortcut method for only showing a field on one page or action:

use Filament\Forms\Components\TextInput;
use Filament\Support\Enums\Operation;

TextInput::make('password')
    ->password()
    ->required()
    ->visibleOn(Operation::Create),

Resource tables

Resource classes contain a table() method that is used to build the table on the List page.

By default, Filament creates a table file for you, which is referenced in the table() method. This is to keep your resource class clean and organized, otherwise it can get quite large:

use App\Filament\Resources\Customers\Tables\CustomersTable;
use Filament\Tables\Table;

public static function table(Table $table): Table
{
    return CustomersTable::configure($table);
}

In the CustomersTable class, you can define the columns, filters and actions of the table:

use Filament\Actions\BulkActionGroup;
use Filament\Actions\DeleteBulkAction;
use Filament\Actions\EditAction;
use Filament\Tables\Columns\TextColumn;
use Filament\Tables\Filters\Filter;
use Filament\Tables\Table;
use Illuminate\Database\Eloquent\Builder;

public static function configure(Table $table): Table
{
    return $table
        ->columns([
            TextColumn::make('name'),
            TextColumn::make('email'),
            // ...
        ])
        ->filters([
            Filter::make('verified')
                ->query(fn (Builder $query): Builder => $query->whereNotNull('email_verified_at')),
            // ...
        ])
        ->recordActions([
            EditAction::make(),
        ])
        ->toolbarActions([
            BulkActionGroup::make([
                DeleteBulkAction::make(),
            ]),
        ]);
}

Check out the tables docs to find out how to add table columns, filters, actions and more.

If you would prefer to define the table directly in the resource class, you can do so and delete the table class altogether:
```php
use Filament\Actions\BulkActionGroup;
use Filament\Actions\DeleteBulkAction;
use Filament\Actions\EditAction;
use Filament\Tables\Columns\TextColumn;
use Filament\Tables\Filters\Filter;
use Filament\Tables\Table;
use Illuminate\Database\Eloquent\Builder;

public static function table(Table $table): Table
{
    return $table
        ->columns([
            TextColumn::make('name'),
            TextColumn::make('email'),
            // ...
        ])
        ->filters([
            Filter::make('verified')
                ->query(fn (Builder $query): Builder => $query->whereNotNull('email_verified_at')),
            // ...
        ])
        ->recordActions([
            EditAction::make(),
        ])
        ->toolbarActions([
            BulkActionGroup::make([
                DeleteBulkAction::make(),
            ]),
        ]);
}
```

Customizing the model label

Each resource has a "model label" which is automatically generated from the model name. For example, an App\Models\Customer model will have a customer label.

The label is used in several parts of the UI, and you may customize it using the $modelLabel property:

protected static ?string $modelLabel = 'cliente';

Alternatively, you may use the getModelLabel() to define a dynamic label:

public static function getModelLabel(): string
{
    return __('filament/resources/customer.label');
}

Customizing the plural model label

Resources also have a "plural model label" which is automatically generated from the model label. For example, a customer label will be pluralized into customers.

You may customize the plural version of the label using the $pluralModelLabel property:

protected static ?string $pluralModelLabel = 'clientes';

Alternatively, you may set a dynamic plural label in the getPluralModelLabel() method:

public static function getPluralModelLabel(): string
{
    return __('filament/resources/customer.plural_label');
}

Automatic model label capitalization

By default, Filament will automatically capitalize each word in the model label, for some parts of the UI. For example, in page titles, the navigation menu, and the breadcrumbs.

If you want to disable this behavior for a resource, you can set $hasTitleCaseModelLabel in the resource:

protected static bool $hasTitleCaseModelLabel = false;

Resource navigation items

Filament will automatically generate a navigation menu item for your resource using the plural label.

If you'd like to customize the navigation item label, you may use the $navigationLabel property:

protected static ?string $navigationLabel = 'Mis Clientes';

Alternatively, you may set a dynamic navigation label in the getNavigationLabel() method:

public static function getNavigationLabel(): string
{
    return __('filament/resources/customer.navigation_label');
}

Setting a resource navigation icon

The $navigationIcon property supports the name of any Blade component. By default, Heroicons are installed. However, you may create your own custom icon components or install an alternative library if you wish.

use BackedEnum;

protected static string | BackedEnum | null $navigationIcon = 'heroicon-o-user-group';

Alternatively, you may set a dynamic navigation icon in the getNavigationIcon() method:

use BackedEnum;
use Illuminate\Contracts\Support\Htmlable;

public static function getNavigationIcon(): string | BackedEnum | Htmlable | null
{
    return 'heroicon-o-user-group';
}

Sorting resource navigation items

The $navigationSort property allows you to specify the order in which navigation items are listed:

protected static ?int $navigationSort = 2;

Alternatively, you may set a dynamic navigation item order in the getNavigationSort() method:

public static function getNavigationSort(): ?int
{
    return 2;
}

Grouping resource navigation items

You may group navigation items by specifying a $navigationGroup property:

use UnitEnum;

protected static string | UnitEnum | null $navigationGroup = 'Shop';

Alternatively, you may use the getNavigationGroup() method to set a dynamic group label:

public static function getNavigationGroup(): ?string
{
    return __('filament/navigation.groups.shop');
}

Grouping resource navigation items under other items

You may group navigation items as children of other items, by passing the label of the parent item as the $navigationParentItem:

use UnitEnum;

protected static ?string $navigationParentItem = 'Products';

protected static string | UnitEnum | null $navigationGroup = 'Shop';

As seen above, if the parent item has a navigation group, that navigation group must also be defined, so the correct parent item can be identified.

You may also use the getNavigationParentItem() method to set a dynamic parent item label:

public static function getNavigationParentItem(): ?string
{
    return __('filament/navigation.groups.shop.items.products');
}
If you're reaching for a third level of navigation like this, you should consider using [clusters](../navigation/clusters) instead, which are a logical grouping of resources and [custom pages](../navigation/custom-pages), which can share their own separate navigation.

Generating URLs to resource pages

Filament provides getUrl() static method on resource classes to generate URLs to resources and specific pages within them. Traditionally, you would need to construct the URL by hand or by using Laravel's route() helper, but these methods depend on knowledge of the resource's slug or route naming conventions.

The getUrl() method, without any arguments, will generate a URL to the resource's List page:

use App\Filament\Resources\Customers\CustomerResource;

CustomerResource::getUrl(); // /admin/customers

You may also generate URLs to specific pages within the resource. The name of each page is the array key in the getPages() array of the resource. For example, to generate a URL to the Create page:

use App\Filament\Resources\Customers\CustomerResource;

CustomerResource::getUrl('create'); // /admin/customers/create

Some pages in the getPages() method use URL parameters like record. To generate a URL to these pages and pass in a record, you should use the second argument:

use App\Filament\Resources\Customers\CustomerResource;

CustomerResource::getUrl('edit', ['record' => $customer]); // /admin/customers/edit/1

In this example, $customer can be an Eloquent model object, or an ID.

Generating URLs to resource modals

This can be especially useful if you are using simple resources with only one page.

To generate a URL for an action in the resource's table, you should pass the tableAction and tableActionRecord as URL parameters:

use App\Filament\Resources\Customers\CustomerResource;
use Filament\Actions\EditAction;

CustomerResource::getUrl(parameters: [
    'tableAction' => EditAction::getDefaultName(),
    'tableActionRecord' => $customer,
]); // /admin/customers?tableAction=edit&tableActionRecord=1

Or if you want to generate a URL for an action on the page like a CreateAction in the header, you can pass it in to the action parameter:

use App\Filament\Resources\Customers\CustomerResource;
use Filament\Actions\CreateAction;

CustomerResource::getUrl(parameters: [
    'action' => CreateAction::getDefaultName(),
]); // /admin/customers?action=create

Generating URLs to resources in other panels

If you have multiple panels in your app, getUrl() will generate a URL within the current panel. You can also indicate which panel the resource is associated with, by passing the panel ID to the panel argument:

use App\Filament\Resources\Customers\CustomerResource;

CustomerResource::getUrl(panel: 'marketing');

Customizing the resource Eloquent query

Within Filament, every query to your resource model will start with the getEloquentQuery() method.

Because of this, it's very easy to apply your own query constraints or model scopes that affect the entire resource:

public static function getEloquentQuery(): Builder
{
    return parent::getEloquentQuery()->where('is_active', true);
}

Disabling global scopes

By default, Filament will observe all global scopes that are registered to your model. However, this may not be ideal if you wish to access, for example, soft-deleted records.

To overcome this, you may override the getEloquentQuery() method that Filament uses:

public static function getEloquentQuery(): Builder
{
    return parent::getEloquentQuery()->withoutGlobalScopes();
}

Alternatively, you may remove specific global scopes:

public static function getEloquentQuery(): Builder
{
    return parent::getEloquentQuery()->withoutGlobalScopes([ActiveScope::class]);
}

More information about removing global scopes may be found in the Laravel documentation.

Customizing the resource URL

By default, Filament will generate a URL based on the name of the resource. You can customize this by setting the $slug property on the resource:

protected static ?string $slug = 'pending-orders';

Resource sub-navigation

Sub-navigation allows the user to navigate between different pages within a resource. Typically, all pages in the sub-navigation will be related to the same record in the resource. For example, in a Customer resource, you may have a sub-navigation with the following pages:

To add a sub-navigation to each "singular record" page in the resource, you can add the getRecordSubNavigation() method to the resource class:

use Filament\Resources\Pages\Page;

public static function getRecordSubNavigation(Page $page): array
{
    return $page->generateNavigationItems([
        ViewCustomer::class,
        EditCustomer::class,
        EditCustomerContact::class,
        ManageCustomerAddresses::class,
        ManageCustomerPayments::class,
    ]);
}

Each item in the sub-navigation can be customized using the same navigation methods as normal pages.

If you're looking to add sub-navigation to switch *between* entire resources and [custom pages](../navigation/custom-pages), you might be looking for [clusters](../navigation/clusters), which are used to group these together. The `getRecordSubNavigation()` method is intended to construct a navigation between pages that relate to a particular record *inside* a resource.

Setting the sub-navigation position for a resource

The sub-navigation is rendered at the start of the page by default. You may change the position for all pages in a resource by setting the $subNavigationPosition property on the resource. The value may be SubNavigationPosition::Start, SubNavigationPosition::End, or SubNavigationPosition::Top to render the sub-navigation as tabs:

use Filament\Pages\Enums\SubNavigationPosition;

protected static ?SubNavigationPosition $subNavigationPosition = SubNavigationPosition::End;

Deleting resource pages

If you'd like to delete a page from your resource, you can just delete the page file from the Pages directory of your resource, and its entry in the getPages() method.

For example, you may have a resource with records that may not be created by anyone. Delete the Create page file, and then remove it from getPages():

public static function getPages(): array
{
    return [
        'index' => ListCustomers::route('/'),
        'edit' => EditCustomer::route('/{record}/edit'),
    ];
}

Deleting a page will not delete any actions that link to that page. Any actions will open a modal instead of sending the user to the non-existent page. For instance, the CreateAction on the List page, the EditAction on the table or View page, or the ViewAction on the table or Edit page. If you want to remove those buttons, you must delete the actions as well.

Security

Authorization

For authorization, Filament will observe any model policies that are registered in your app. The following methods are used:

  • viewAny() is used to completely hide resources from the navigation menu, and prevents the user from accessing any pages.
  • create() is used to control creating new records.
  • update() is used to control editing a record.
  • view() is used to control viewing a record.
  • delete() is used to prevent a single record from being deleted. deleteAny() is used to prevent records from being bulk deleted. Filament uses the deleteAny() method because iterating through multiple records and checking the delete() policy is not very performant. When using a DeleteBulkAction, if you want to call the delete() method for each record anyway, you should use the DeleteBulkAction::make()->authorizeIndividualRecords() method. Any records that fail the authorization check will not be processed.
  • forceDelete() is used to prevent a single soft-deleted record from being force-deleted. forceDeleteAny() is used to prevent records from being bulk force-deleted. Filament uses the forceDeleteAny() method because iterating through multiple records and checking the forceDelete() policy is not very performant. When using a ForceDeleteBulkAction, if you want to call the forceDelete() method for each record anyway, you should use the ForceDeleteBulkAction::make()->authorizeIndividualRecords() method. Any records that fail the authorization check will not be processed.
  • restore() is used to prevent a single soft-deleted record from being restored. restoreAny() is used to prevent records from being bulk restored. Filament uses the restoreAny() method because iterating through multiple records and checking the restore() policy is not very performant. When using a RestoreBulkAction, if you want to call the restore() method for each record anyway, you should use the RestoreBulkAction::make()->authorizeIndividualRecords() method. Any records that fail the authorization check will not be processed.
  • reorder() is used to control reordering records in a table.

Skipping authorization

If you'd like to skip authorization for a resource, you may set the $shouldSkipAuthorization property to true:

protected static bool $shouldSkipAuthorization = true;

Protecting model attributes

Filament will expose all model attributes to JavaScript, except if they are $hidden on your model. This is Livewire's behavior for model binding. We preserve this functionality to facilitate the dynamic addition and removal of form fields after they are initially loaded, while preserving the data they may need.

While attributes may be visible in JavaScript, only those with a form field are actually editable by the user. This is not an issue with mass assignment.

To remove certain attributes from JavaScript on the Edit and View pages, you may override the mutateFormDataBeforeFill() method:

protected function mutateFormDataBeforeFill(array $data): array
{
    unset($data['is_admin']);

    return $data;
}

In this example, we remove the is_admin attribute from JavaScript, as it's not being used by the form.



title: Listing records

Using tabs to filter the records

You can add tabs above the table, which can be used to filter the records based on some predefined conditions. Each tab can scope the Eloquent query of the table in a different way. To register tabs, add a getTabs() method to the List page class, and return an array of Tab objects:

use Filament\Schemas\Components\Tabs\Tab;
use Illuminate\Database\Eloquent\Builder;

public function getTabs(): array
{
    return [
        'all' => Tab::make(),
        'active' => Tab::make()
            ->modifyQueryUsing(fn (Builder $query) => $query->where('active', true)),
        'inactive' => Tab::make()
            ->modifyQueryUsing(fn (Builder $query) => $query->where('active', false)),
    ];
}

Customizing the filter tab labels

The keys of the array will be used as identifiers for the tabs, so they can be persisted in the URL's query string. The label of each tab is also generated from the key, but you can override that by passing a label into the make() method of the tab:

use Filament\Schemas\Components\Tabs\Tab;
use Illuminate\Database\Eloquent\Builder;

public function getTabs(): array
{
    return [
        'all' => Tab::make('All customers'),
        'active' => Tab::make('Active customers')
            ->modifyQueryUsing(fn (Builder $query) => $query->where('active', true)),
        'inactive' => Tab::make('Inactive customers')
            ->modifyQueryUsing(fn (Builder $query) => $query->where('active', false)),
    ];
}

Adding icons to filter tabs

You can add icons to the tabs by passing an icon into the icon() method of the tab:

use Filament\Schemas\Components\Tabs\Tab;

Tab::make()
    ->icon('heroicon-m-user-group')

You can also change the icon's position to be after the label instead of before it, using the iconPosition() method:

use Filament\Support\Enums\IconPosition;

Tab::make()
    ->icon('heroicon-m-user-group')
    ->iconPosition(IconPosition::After)

Adding badges to filter tabs

You can add badges to the tabs by passing a string into the badge() method of the tab:

use Filament\Schemas\Components\Tabs\Tab;

Tab::make()
    ->badge(Customer::query()->where('active', true)->count())

Changing the color of filter tab badges

The color of a badge may be changed using the badgeColor() method:

use Filament\Schemas\Components\Tabs\Tab;

Tab::make()
    ->badge(Customer::query()->where('active', true)->count())
    ->badgeColor('success')

Adding extra attributes to filter tabs

You may also pass extra HTML attributes to filter tabs using extraAttributes():

use Filament\Schemas\Components\Tabs\Tab;

Tab::make()
    ->extraAttributes(['data-cy' => 'statement-confirmed-tab'])

Customizing the default tab

To customize the default tab that is selected when the page is loaded, you can return the array key of the tab from the getDefaultActiveTab() method:

use Filament\Schemas\Components\Tabs\Tab;

public function getTabs(): array
{
    return [
        'all' => Tab::make(),
        'active' => Tab::make(),
        'inactive' => Tab::make(),
    ];
}

public function getDefaultActiveTab(): string | int | null
{
    return 'active';
}

Authorization

For authorization, Filament will observe any model policies that are registered in your app.

Users may access the List page if the viewAny() method of the model policy returns true.

The reorder() method is used to control reordering a record.

Customizing the table Eloquent query

Although you can customize the Eloquent query for the entire resource, you may also make specific modifications for the List page table. To do this, use the modifyQueryUsing() method in the table() method of the resource:

use Filament\Tables\Table;
use Illuminate\Database\Eloquent\Builder;

public static function table(Table $table): Table
{
    return $table
        ->modifyQueryUsing(fn (Builder $query) => $query->withoutGlobalScopes());
}

Custom page content

Each page in Filament has its own schema, which defines the overall structure and content. You can override the schema for the page by defining a content() method on it. The content() method for the List page contains the following components by default:

use Filament\Schemas\Components\EmbeddedTable;
use Filament\Schemas\Components\RenderHook;
use Filament\Schemas\Schema;

public function content(Schema $schema): Schema
{
    return $schema
        ->components([
            $this->getTabsContentComponent(), // This method returns a component to display the tabs above a table
            RenderHook::make(PanelsRenderHook::RESOURCE_PAGES_LIST_RECORDS_TABLE_BEFORE),
            EmbeddedTable::make(), // This is the component that renders the table that is defined in this resource
            RenderHook::make(PanelsRenderHook::RESOURCE_PAGES_LIST_RECORDS_TABLE_AFTER),
        ]);
}

Inside the components() array, you can insert any schema component. You can reorder the components by changing the order of the array or remove any of the components that are not needed.

Using a custom Blade view

For further customization opportunities, you can override the static $view property on the page class to a custom view in your app:

protected string $view = 'filament.resources.users.pages.list-users';

This assumes that you have created a view at resources/views/filament/resources/users/pages/list-users.blade.php:

<x-filament-panels::page>
    {{ $this->content }} {{-- This will render the content of the page defined in the `content()` method, which can be removed if you want to start from scratch --}}
</x-filament-panels::page>


title: Creating records

Customizing data before saving

Sometimes, you may wish to modify form data before it is finally saved to the database. To do this, you may define a mutateFormDataBeforeCreate() method on the Create page class, which accepts the $data as an array, and returns the modified version:

protected function mutateFormDataBeforeCreate(array $data): array
{
    $data['user_id'] = auth()->id();

    return $data;
}

Alternatively, if you're creating records in a modal action, check out the Actions documentation.

Customizing the creation process

You can tweak how the record is created using the handleRecordCreation() method on the Create page class:

use Illuminate\Database\Eloquent\Model;

protected function handleRecordCreation(array $data): Model
{
    return static::getModel()::create($data);
}

Alternatively, if you're creating records in a modal action, check out the Actions documentation.

Customizing redirects

By default, after saving the form, the user will be redirected to the Edit page of the resource, or the View page if it is present.

You may set up a custom redirect when the form is saved by overriding the getRedirectUrl() method on the Create page class.

For example, the form can redirect back to the List page:

protected function getRedirectUrl(): string
{
    return $this->getResource()::getUrl('index');
}

If you wish to be redirected to the previous page, else the index page:

protected function getRedirectUrl(): string
{
    return $this->previousUrl ?? $this->getResource()::getUrl('index');
}

You can also use the configuration to customize the default redirect page for all resources at once:

use Filament\Panel;

public function panel(Panel $panel): Panel
{
    return $panel
        // ...
        ->resourceCreatePageRedirect('index') // or
        ->resourceCreatePageRedirect('view') // or
        ->resourceCreatePageRedirect('edit');
}

Customizing the save notification

When the record is successfully created, a notification is dispatched to the user, which indicates the success of their action.

To customize the title of this notification, define a getCreatedNotificationTitle() method on the create page class:

protected function getCreatedNotificationTitle(): ?string
{
    return 'User registered';
}

Alternatively, if you're creating records in a modal action, check out the Actions documentation.

You may customize the entire notification by overriding the getCreatedNotification() method on the create page class:

use Filament\Notifications\Notification;

protected function getCreatedNotification(): ?Notification
{
    return Notification::make()
        ->success()
        ->title('User registered')
        ->body('The user has been created successfully.');
}

To disable the notification altogether, return null from the getCreatedNotification() method on the create page class:

use Filament\Notifications\Notification;

protected function getCreatedNotification(): ?Notification
{
    return null;
}

Creating another record

Disabling create another

To disable the "create and create another" feature, define the $canCreateAnother property as false on the Create page class:

protected static bool $canCreateAnother = false;

Alternatively, if you'd like to specify a dynamic condition when the feature is disabled, you may override the canCreateAnother() method on the Create page class:

public function canCreateAnother(): bool
{
    return false;
}

Preserving data when creating another

By default, when the user uses the "create and create another" feature, all the form data is cleared so the user can start fresh. If you'd like to preserve some of the data in the form, you may override the preserveFormDataWhenCreatingAnother() method on the Create page class, and return the part of the $data array that you'd like to keep:

use Illuminate\Support\Arr;

protected function preserveFormDataWhenCreatingAnother(array $data): array
{
    return Arr::only($data, ['is_admin', 'organization']);
}

To preserve all the data, return the entire $data array:

protected function preserveFormDataWhenCreatingAnother(array $data): array
{
    return $data;
}

Lifecycle hooks

Hooks may be used to execute code at various points within a page's lifecycle, like before a form is saved. To set up a hook, create a protected method on the Create page class with the name of the hook:

protected function beforeCreate(): void
{
    // ...
}

In this example, the code in the beforeCreate() method will be called before the data in the form is saved to the database.

There are several available hooks for the Create page:

use Filament\Resources\Pages\CreateRecord;

class CreateUser extends CreateRecord
{
    // ...

    protected function beforeFill(): void
    {
        // Runs before the form fields are populated with their default values.
    }

    protected function afterFill(): void
    {
        // Runs after the form fields are populated with their default values.
    }

    protected function beforeValidate(): void
    {
        // Runs before the form fields are validated when the form is submitted.
    }

    protected function afterValidate(): void
    {
        // Runs after the form fields are validated when the form is submitted.
    }

    protected function beforeCreate(): void
    {
        // Runs before the form fields are saved to the database.
    }

    protected function afterCreate(): void
    {
        // Runs after the form fields are saved to the database.
    }
}

Alternatively, if you're creating records in a modal action, check out the Actions documentation.

Halting the creation process

At any time, you may call $this->halt() from inside a lifecycle hook or mutation method, which will halt the entire creation process:

use Filament\Actions\Action;
use Filament\Notifications\Notification;

protected function beforeCreate(): void
{
    if (! auth()->user()->team->subscribed()) {
        Notification::make()
            ->warning()
            ->title('You don\'t have an active subscription!')
            ->body('Choose a plan to continue.')
            ->persistent()
            ->actions([
                Action::make('subscribe')
                    ->button()
                    ->url(route('subscribe'), shouldOpenInNewTab: true),
            ])
            ->send();
    
        $this->halt();
    }
}

Alternatively, if you're creating records in a modal action, check out the Actions documentation.

Authorization

For authorization, Filament will observe any model policies that are registered in your app.

Users may access the Create page if the create() method of the model policy returns true.

Using a wizard

You may easily transform the creation process into a multistep wizard.

On the page class, add the corresponding HasWizard trait:

use App\Filament\Resources\Categories\CategoryResource;
use Filament\Resources\Pages\CreateRecord;

class CreateCategory extends CreateRecord
{
    use CreateRecord\Concerns\HasWizard;
    
    protected static string $resource = CategoryResource::class;

    protected function getSteps(): array
    {
        return [
            // ...
        ];
    }
}

Inside the getSteps() array, return your wizard steps:

use Filament\Forms\Components\MarkdownEditor;
use Filament\Forms\Components\TextInput;
use Filament\Forms\Components\Toggle;
use Filament\Schemas\Components\Wizard\Step;

protected function getSteps(): array
{
    return [
        Step::make('Name')
            ->description('Give the category a clear and unique name')
            ->schema([
                TextInput::make('name')
                    ->required()
                    ->live()
                    ->afterStateUpdated(fn ($state, callable $set) => $set('slug', Str::slug($state))),
                TextInput::make('slug')
                    ->disabled()
                    ->required()
                    ->unique(Category::class, 'slug', fn ($record) => $record),
            ]),
        Step::make('Description')
            ->description('Add some extra details')
            ->schema([
                MarkdownEditor::make('description')
                    ->columnSpan('full'),
            ]),
        Step::make('Visibility')
            ->description('Control who can view it')
            ->schema([
                Toggle::make('is_visible')
                    ->label('Visible to customers.')
                    ->default(true),
            ]),
    ];
}

Alternatively, if you're creating records in a modal action, check out the Actions documentation.

Now, create a new record to see your wizard in action! Edit will still use the form defined within the resource class.

If you'd like to allow free navigation, so all the steps are skippable, override the hasSkippableSteps() method:

public function hasSkippableSteps(): bool
{
    return true;
}

Sharing fields between the form schema and wizards

If you'd like to reduce the amount of repetition between the resource form and wizard steps, it's a good idea to extract public static form functions for your fields, where you can easily retrieve an instance of a field from the form schema or the wizard:

use Filament\Forms;
use Filament\Schemas\Schema;

class CategoryForm
{
    public static function configure(Schema $schema): Schema
    {
        return $schema
            ->components([
                static::getNameFormField(),
                static::getSlugFormField(),
                // ...
            ]);
    }
    
    public static function getNameFormField(): Forms\Components\TextInput
    {
        return TextInput::make('name')
            ->required()
            ->live()
            ->afterStateUpdated(fn ($state, callable $set) => $set('slug', Str::slug($state)));
    }
    
    public static function getSlugFormField(): Forms\Components\TextInput
    {
        return TextInput::make('slug')
            ->disabled()
            ->required()
            ->unique(Category::class, 'slug', fn ($record) => $record);
    }
}
use App\Filament\Resources\Categories\Schemas\CategoryForm;
use Filament\Resources\Pages\CreateRecord;

class CreateCategory extends CreateRecord
{
    use CreateRecord\Concerns\HasWizard;
    
    protected static string $resource = CategoryResource::class;

    protected function getSteps(): array
    {
        return [
            Step::make('Name')
                ->description('Give the category a clear and unique name')
                ->schema([
                    CategoryForm::getNameFormField(),
                    CategoryForm::getSlugFormField(),
                ]),
            // ...
        ];
    }
}

Importing resource records

Filament includes an ImportAction that you can add to the getHeaderActions() of the List page. It allows users to upload a CSV of data to import into the resource:

use App\Filament\Imports\ProductImporter;
use Filament\Actions;

protected function getHeaderActions(): array
{
    return [
        Actions\ImportAction::make()
            ->importer(ProductImporter::class),
        Actions\CreateAction::make(),
    ];
}

The "importer" class needs to be created to tell Filament how to import each row of the CSV. You can learn everything about the ImportAction in the Actions documentation.

Custom actions

"Actions" are buttons that are displayed on pages, which allow the user to run a Livewire method on the page or visit a URL.

On resource pages, actions are usually in 2 places: in the top right of the page, and below the form.

For example, you may add a new button action in the header of the Create page:

use App\Filament\Imports\UserImporter;
use Filament\Actions;
use Filament\Resources\Pages\CreateRecord;

class CreateUser extends CreateRecord
{
    // ...

    protected function getHeaderActions(): array
    {
        return [
            Actions\ImportAction::make()
                ->importer(UserImporter::class),
        ];
    }
}

Or, a new button next to "Create" below the form:

use Filament\Actions\Action;
use Filament\Resources\Pages\CreateRecord;

class CreateUser extends CreateRecord
{
    // ...

    protected function getFormActions(): array
    {
        return [
            ...parent::getFormActions(),
            Action::make('close')->action('createAndClose'),
        ];
    }

    public function createAndClose(): void
    {
        // ...
    }
}

To view the entire actions API, please visit the pages section.

Adding a create action button to the header

The "Create" button can be moved to the header of the page by overriding the getHeaderActions() method and using getCreateFormAction(). You need to pass formId() to the action, to specify that the action should submit the form with the ID of form, which is the <form> ID used in the view of the page:

protected function getHeaderActions(): array
{
    return [
        $this->getCreateFormAction()
            ->formId('form'),
    ];
}

You may remove all actions from the form by overriding the getFormActions() method to return an empty array:

protected function getFormActions(): array
{
    return [];
}

Custom page content

Each page in Filament has its own schema, which defines the overall structure and content. You can override the schema for the page by defining a content() method on it. The content() method for the Create page contains the following components by default:

use Filament\Schemas\Schema;

public function content(Schema $schema): Schema
{
    return $schema
        ->components([
            $this->getFormContentComponent(), // This method returns a component to display the form that is defined in this resource
        ]);
}

Inside the components() array, you can insert any schema component. You can reorder the components by changing the order of the array or remove any of the components that are not needed.

Using a custom Blade view

For further customization opportunities, you can override the static $view property on the page class to a custom view in your app:

protected string $view = 'filament.resources.users.pages.create-user';

This assumes that you have created a view at resources/views/filament/resources/users/pages/create-user.blade.php:

<x-filament-panels::page>
    {{ $this->content }} {{-- This will render the content of the page defined in the `content()` method, which can be removed if you want to start from scratch --}}
</x-filament-panels::page>


title: Editing records

Customizing data before filling the form

You may wish to modify the data from a record before it is filled into the form. To do this, you may define a mutateFormDataBeforeFill() method on the Edit page class to modify the $data array, and return the modified version before it is filled into the form:

protected function mutateFormDataBeforeFill(array $data): array
{
    $data['user_id'] = auth()->id();

    return $data;
}

Alternatively, if you're editing records in a modal action, check out the Actions documentation.

Customizing data before saving

Sometimes, you may wish to modify form data before it is finally saved to the database. To do this, you may define a mutateFormDataBeforeSave() method on the Edit page class, which accepts the $data as an array, and returns it modified:

protected function mutateFormDataBeforeSave(array $data): array
{
    $data['last_edited_by_id'] = auth()->id();

    return $data;
}

Alternatively, if you're editing records in a modal action, check out the Actions documentation.

Customizing the saving process

You can tweak how the record is updated using the handleRecordUpdate() method on the Edit page class:

use Illuminate\Database\Eloquent\Model;

protected function handleRecordUpdate(Model $record, array $data): Model
{
    $record->update($data);

    return $record;
}

Alternatively, if you're editing records in a modal action, check out the Actions documentation.

Customizing redirects

By default, saving the form will not redirect the user to another page.

You may set up a custom redirect when the form is saved by overriding the getRedirectUrl() method on the Edit page class.

For example, the form can redirect back to the List page of the resource:

protected function getRedirectUrl(): string
{
    return $this->getResource()::getUrl('index');
}

Or the View page:

protected function getRedirectUrl(): string
{
    return $this->getResource()::getUrl('view', ['record' => $this->getRecord()]);
}

If you wish to be redirected to the previous page, else the index page:

protected function getRedirectUrl(): string
{
    return $this->previousUrl ?? $this->getResource()::getUrl('index');
}

You can also use the configuration to customize the default redirect page for all resources at once:

use Filament\Panel;

public function panel(Panel $panel): Panel
{
    return $panel
        // ...
        ->resourceEditPageRedirect('index') // or
        ->resourceEditPageRedirect('view');
}

Customizing the save notification

When the record is successfully updated, a notification is dispatched to the user, which indicates the success of their action.

To customize the title of this notification, define a getSavedNotificationTitle() method on the edit page class:

protected function getSavedNotificationTitle(): ?string
{
    return 'User updated';
}

Alternatively, if you're editing records in a modal action, check out the Actions documentation.

You may customize the entire notification by overriding the getSavedNotification() method on the edit page class:

use Filament\Notifications\Notification;

protected function getSavedNotification(): ?Notification
{
    return Notification::make()
        ->success()
        ->title('User updated')
        ->body('The user has been saved successfully.');
}

To disable the notification altogether, return null from the getSavedNotification() method on the edit page class:

use Filament\Notifications\Notification;

protected function getSavedNotification(): ?Notification
{
    return null;
}

Lifecycle hooks

Hooks may be used to execute code at various points within a page's lifecycle, like before a form is saved. To set up a hook, create a protected method on the Edit page class with the name of the hook:

protected function beforeSave(): void
{
    // ...
}

In this example, the code in the beforeSave() method will be called before the data in the form is saved to the database.

There are several available hooks for the Edit pages:

use Filament\Resources\Pages\EditRecord;

class EditUser extends EditRecord
{
    // ...

    protected function beforeFill(): void
    {
        // Runs before the form fields are populated from the database.
    }

    protected function afterFill(): void
    {
        // Runs after the form fields are populated from the database.
    }

    protected function beforeValidate(): void
    {
        // Runs before the form fields are validated when the form is saved.
    }

    protected function afterValidate(): void
    {
        // Runs after the form fields are validated when the form is saved.
    }

    protected function beforeSave(): void
    {
        // Runs before the form fields are saved to the database.
    }

    protected function afterSave(): void
    {
        // Runs after the form fields are saved to the database.
    }
}

Alternatively, if you're editing records in a modal action, check out the Actions documentation.

Saving a part of the form independently

You may want to allow the user to save a part of the form independently of the rest of the form. One way to do this is with a section action in the header or footer. From the action() method, you can call saveFormComponentOnly(), passing in the Section component that you want to save:

use Filament\Actions\Action;
use Filament\Notifications\Notification;
use Filament\Resources\Pages\EditRecord;
use Filament\Schemas\Components\Section;

Section::make('Rate limiting')
    ->schema([
        // ...
    ])
    ->footerActions([
        fn (string $operation): Action => Action::make('save')
            ->action(function (Section $component, EditRecord $livewire) {
                $livewire->saveFormComponentOnly($component);
                
                Notification::make()
                    ->title('Rate limiting saved')
                    ->body('The rate limiting settings have been saved successfully.')
                    ->success()
                    ->send();
            })
            ->visible($operation === 'edit'),
    ])

The $operation helper is available, to ensure that the action is only visible when the form is being edited.

Halting the saving process

At any time, you may call $this->halt() from inside a lifecycle hook or mutation method, which will halt the entire saving process:

use Filament\Actions\Action;
use Filament\Notifications\Notification;

protected function beforeSave(): void
{
    if (! $this->getRecord()->team->subscribed()) {
        Notification::make()
            ->warning()
            ->title('You don\'t have an active subscription!')
            ->body('Choose a plan to continue.')
            ->persistent()
            ->actions([
                Action::make('subscribe')
                    ->button()
                    ->url(route('subscribe'), shouldOpenInNewTab: true),
            ])
            ->send();

        $this->halt();
    }
}

Alternatively, if you're editing records in a modal action, check out the Actions documentation.

Authorization

For authorization, Filament will observe any model policies that are registered in your app.

Users may access the Edit page if the update() method of the model policy returns true.

They also have the ability to delete the record if the delete() method of the policy returns true.

Custom actions

"Actions" are buttons that are displayed on pages, which allow the user to run a Livewire method on the page or visit a URL.

On resource pages, actions are usually in 2 places: in the top right of the page, and below the form.

For example, you may add a new button action next to "Delete" on the Edit page:

use Filament\Actions;
use Filament\Resources\Pages\EditRecord;

class EditUser extends EditRecord
{
    // ...

    protected function getHeaderActions(): array
    {
        return [
            Actions\Action::make('impersonate')
                ->action(function (): void {
                    // ...
                }),
            Actions\DeleteAction::make(),
        ];
    }
}

Or, a new button next to "Save" below the form:

use Filament\Actions\Action;
use Filament\Resources\Pages\EditRecord;

class EditUser extends EditRecord
{
    // ...

    protected function getFormActions(): array
    {
        return [
            ...parent::getFormActions(),
            Action::make('close')->action('saveAndClose'),
        ];
    }

    public function saveAndClose(): void
    {
        // ...
    }
}

To view the entire actions API, please visit the pages section.

Adding a save action button to the header

The "Save" button can be added to the header of the page by overriding the getHeaderActions() method and using getSaveFormAction(). You need to pass formId() to the action, to specify that the action should submit the form with the ID of form, which is the <form> ID used in the view of the page:

protected function getHeaderActions(): array
{
    return [
        $this->getSaveFormAction()
            ->formId('form'),
    ];
}

You may remove all actions from the form by overriding the getFormActions() method to return an empty array:

protected function getFormActions(): array
{
    return [];
}

Creating another Edit page

One Edit page may not be enough space to allow users to navigate many form fields. You can create as many Edit pages for a resource as you want. This is especially useful if you are using resource sub-navigation, as you are then easily able to switch between the different Edit pages.

To create an Edit page, you should use the make:filament-page command:

php artisan make:filament-page EditCustomerContact --resource=CustomerResource --type=EditRecord

You must register this new page in your resource's getPages() method:

public static function getPages(): array
{
    return [
        'index' => Pages\ListCustomers::route('/'),
        'create' => Pages\CreateCustomer::route('/create'),
        'view' => Pages\ViewCustomer::route('/{record}'),
        'edit' => Pages\EditCustomer::route('/{record}/edit'),
        'edit-contact' => Pages\EditCustomerContact::route('/{record}/edit/contact'),
    ];
}

Now, you can define the form() for this page, which can contain other fields that are not present on the main Edit page:

use Filament\Schemas\Schema;

public function form(Schema $schema): Schema
{
    return $schema
        ->components([
            // ...
        ]);
}

Adding edit pages to resource sub-navigation

If you're using resource sub-navigation, you can register this page as normal in getRecordSubNavigation() of the resource:

use App\Filament\Resources\Customers\Pages;
use Filament\Resources\Pages\Page;

public static function getRecordSubNavigation(Page $page): array
{
    return $page->generateNavigationItems([
        // ...
        Pages\EditCustomerContact::class,
    ]);
}

Custom page content

Each page in Filament has its own schema, which defines the overall structure and content. You can override the schema for the page by defining a content() method on it. The content() method for the Edit page contains the following components by default:

use Filament\Schemas\Schema;

public function content(Schema $schema): Schema
{
    return $schema
        ->components([
            $this->getFormContentComponent(), // This method returns a component to display the form that is defined in this resource
            $this->getRelationManagersContentComponent(), // This method returns a component to display the relation managers that are defined in this resource
        ]);
}

Inside the components() array, you can insert any schema component. You can reorder the components by changing the order of the array or remove any of the components that are not needed.

Using a custom Blade view

For further customization opportunities, you can override the static $view property on the page class to a custom view in your app:

protected string $view = 'filament.resources.users.pages.edit-user';

This assumes that you have created a view at resources/views/filament/resources/users/pages/edit-user.blade.php:

<x-filament-panels::page>
    {{-- `$this->getRecord()` will return the current Eloquent record for this page --}}
    
    {{ $this->content }} {{-- This will render the content of the page defined in the `content()` method, which can be removed if you want to start from scratch --}}
</x-filament-panels::page>


title: Viewing records

Creating a resource with a View page

To create a new resource with a View page, you can use the --view flag:

php artisan make:filament-resource User --view

Using an infolist instead of a disabled form

By default, the View page will display a disabled form with the record's data. If you preferred to display the record's data in an "infolist", you can define an infolist() method on the resource class:

use Filament\Infolists;
use Filament\Schemas\Schema;

public static function infolist(Schema $schema): Schema
{
    return $schema
        ->components([
            Infolists\Components\TextEntry::make('name'),
            Infolists\Components\TextEntry::make('email'),
            Infolists\Components\TextEntry::make('notes')
                ->columnSpanFull(),
        ]);
}

The components() method is used to define the structure of your infolist. It is an array of entries and layout components, in the order they should appear in your infolist.

Check out the Infolists docs for a guide on how to build infolists with Filament.

Adding a View page to an existing resource

If you want to add a View page to an existing resource, create a new page in your resource's Pages directory:

php artisan make:filament-page ViewUser --resource=UserResource --type=ViewRecord

You must register this new page in your resource's getPages() method:

public static function getPages(): array
{
    return [
        'index' => Pages\ListUsers::route('/'),
        'create' => Pages\CreateUser::route('/create'),
        'view' => Pages\ViewUser::route('/{record}'),
        'edit' => Pages\EditUser::route('/{record}/edit'),
    ];
}

Viewing records in modals

If your resource is simple, you may wish to view records in modals rather than on the View page. If this is the case, you can just delete the view page.

If your resource doesn't contain a ViewAction, you can add one to the $table->recordActions() array:

use Filament\Actions\ViewAction;
use Filament\Tables\Table;

public static function table(Table $table): Table
{
    return $table
        ->columns([
            // ...
        ])
        ->recordActions([
            ViewAction::make(),
            // ...
        ]);
}

Customizing data before filling the form

You may wish to modify the data from a record before it is filled into the form. To do this, you may define a mutateFormDataBeforeFill() method on the View page class to modify the $data array, and return the modified version before it is filled into the form:

protected function mutateFormDataBeforeFill(array $data): array
{
    $data['user_id'] = auth()->id();

    return $data;
}

Alternatively, if you're viewing records in a modal action, check out the Actions documentation.

Lifecycle hooks

Hooks may be used to execute code at various points within a page's lifecycle, like before a form is filled. To set up a hook, create a protected method on the View page class with the name of the hook:

use Filament\Resources\Pages\ViewRecord;

class ViewUser extends ViewRecord
{
    // ...

    protected function beforeFill(): void
    {
        // Runs before the disabled form fields are populated from the database. Not run on pages using an infolist.
    }

    protected function afterFill(): void
    {
        // Runs after the disabled form fields are populated from the database. Not run on pages using an infolist.
    }
}

Authorization

For authorization, Filament will observe any model policies that are registered in your app.

Users may access the View page if the view() method of the model policy returns true.

Creating another View page

One View page may not be enough space to allow users to navigate a lot of information. You can create as many View pages for a resource as you want. This is especially useful if you are using resource sub-navigation, as you are then easily able to switch between the different View pages.

To create a View page, you should use the make:filament-page command:

php artisan make:filament-page ViewCustomerContact --resource=CustomerResource --type=ViewRecord

You must register this new page in your resource's getPages() method:

public static function getPages(): array
{
    return [
        'index' => Pages\ListCustomers::route('/'),
        'create' => Pages\CreateCustomer::route('/create'),
        'view' => Pages\ViewCustomer::route('/{record}'),
        'view-contact' => Pages\ViewCustomerContact::route('/{record}/contact'),
        'edit' => Pages\EditCustomer::route('/{record}/edit'),
    ];
}

Now, you can define the infolist() or form() for this page, which can contain other components that are not present on the main View page:

use Filament\Schemas\Schema;

public function infolist(Schema $schema): Schema
{
    return $schema
        ->components([
            // ...
        ]);
}

Customizing relation managers for a specific view page

You can specify which relation managers should appear on a view page by defining a getAllRelationManagers() method:

protected function getAllRelationManagers(): array
{
    return [
        CustomerAddressesRelationManager::class,
        CustomerContactsRelationManager::class,
    ];
}

This is useful when you have multiple view pages and need different relation managers on each page:

// ViewCustomer.php
protected function getAllRelationManagers(): array
{
    return [
        RelationManagers\OrdersRelationManager::class,
        RelationManagers\SubscriptionsRelationManager::class,
    ];
}

// ViewCustomerContact.php 
protected function getAllRelationManagers(): array
{
    return [
        RelationManagers\ContactsRelationManager::class,
        RelationManagers\AddressesRelationManager::class,
    ];
}

If getAllRelationManagers() isn't defined, any relation managers defined in the resource will be used.

Adding view pages to resource sub-navigation

If you're using resource sub-navigation, you can register this page as normal in getRecordSubNavigation() of the resource:

use App\Filament\Resources\Customers\Pages;
use Filament\Resources\Pages\Page;

public static function getRecordSubNavigation(Page $page): array
{
    return $page->generateNavigationItems([
        // ...
        Pages\ViewCustomerContact::class,
    ]);
}

Custom page content

Each page in Filament has its own schema, which defines the overall structure and content. You can override the schema for the page by defining a content() method on it. The content() method for the View page contains the following components by default:

use Filament\Schemas\Schema;

public function content(Schema $schema): Schema
{
    return $schema
        ->components([
            $this->hasInfolist() // This method returns `true` if the page has an infolist defined
                ? $this->getInfolistContentComponent() // This method returns a component to display the infolist that is defined in this resource
                : $this->getFormContentComponent(), // This method returns a component to display the form that is defined in this resource
            $this->getRelationManagersContentComponent(), // This method returns a component to display the relation managers that are defined in this resource
        ]);
}

Inside the components() array, you can insert any schema component. You can reorder the components by changing the order of the array or remove any of the components that are not needed.

Using a custom Blade view

For further customization opportunities, you can override the static $view property on the page class to a custom view in your app:

protected string $view = 'filament.resources.users.pages.view-user';

This assumes that you have created a view at resources/views/filament/resources/users/pages/view-user.blade.php:

<x-filament-panels::page>
    {{-- `$this->getRecord()` will return the current Eloquent record for this page --}}
    
    {{ $this->content }} {{-- This will render the content of the page defined in the `content()` method, which can be removed if you want to start from scratch --}}
</x-filament-panels::page>


title: Deleting records

Handling soft-deletes

Creating a resource with soft-delete

By default, you will not be able to interact with deleted records in the app. If you'd like to add functionality to restore, force-delete and filter trashed records in your resource, use the --soft-deletes flag when generating the resource:

php artisan make:filament-resource Customer --soft-deletes

Adding soft-deletes to an existing resource

Alternatively, you may add soft-deleting functionality to an existing resource.

Firstly, you must update the resource:

use Filament\Actions\BulkActionGroup;
use Filament\Actions\DeleteAction;
use Filament\Actions\DeleteBulkAction;
use Filament\Actions\ForceDeleteAction;
use Filament\Actions\ForceDeleteBulkAction;
use Filament\Actions\RestoreAction;
use Filament\Actions\RestoreBulkAction;
use Filament\Tables\Filters\TrashedFilter;
use Filament\Tables\Table;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\SoftDeletingScope;

public static function table(Table $table): Table
{
    return $table
        ->columns([
            // ...
        ])
        ->filters([
            TrashedFilter::make(),
            // ...
        ])
        ->recordActions([
            // You may add these actions to your table if you're using a simple
            // resource, or you just want to be able to delete records without
            // leaving the table.
            DeleteAction::make(),
            ForceDeleteAction::make(),
            RestoreAction::make(),
            // ...
        ])
        ->toolbarActions([
            BulkActionGroup::make([
                DeleteBulkAction::make(),
                ForceDeleteBulkAction::make(),
                RestoreBulkAction::make(),
                // ...
            ]),
        ]);
}

public static function getRecordRouteBindingEloquentQuery(): Builder
{
    return parent::getRecordRouteBindingEloquentQuery()
        ->withoutGlobalScopes([
            SoftDeletingScope::class,
        ]);
}

Now, update the Edit page class if you have one:

use Filament\Actions;

protected function getHeaderActions(): array
{
    return [
        Actions\DeleteAction::make(),
        Actions\ForceDeleteAction::make(),
        Actions\RestoreAction::make(),
        // ...
    ];
}

Deleting records on the List page

By default, you can bulk-delete records in your table. You may also wish to delete single records, using a DeleteAction:

use Filament\Actions\DeleteAction;
use Filament\Tables\Table;

public static function table(Table $table): Table
{
    return $table
        ->columns([
            // ...
        ])
        ->recordActions([
            // ...
            DeleteAction::make(),
        ]);
}

Authorization

For authorization, Filament will observe any model policies that are registered in your app.

Users may delete records if the delete() method of the model policy returns true.

They also have the ability to bulk-delete records if the deleteAny() method of the policy returns true. Filament uses the deleteAny() method because iterating through multiple records and checking the delete() policy is not very performant.

You can use the authorizeIndividualRecords() method on the BulkDeleteAction to check the delete() policy for each record individually.

Authorizing soft-deletes

The forceDelete() policy method is used to prevent a single soft-deleted record from being force-deleted. forceDeleteAny() is used to prevent records from being bulk force-deleted. Filament uses the forceDeleteAny() method because iterating through multiple records and checking the forceDelete() policy is not very performant.

The restore() policy method is used to prevent a single soft-deleted record from being restored. restoreAny() is used to prevent records from being bulk restored. Filament uses the restoreAny() method because iterating through multiple records and checking the restore() policy is not very performant.



title: Managing relationships

import Aside from "@components/Aside.astro"

Choosing the right tool for the job

Filament provides many ways to manage relationships in the app. Which feature you should use depends on the type of relationship you are managing, and which UI you are looking for.

Relation managers - interactive tables underneath your resource forms

These are compatible with `HasMany`, `HasManyThrough`, `BelongsToMany`, `MorphMany` and `MorphToMany` relationships.

Relation managers are interactive tables that allow administrators to list, create, attach, associate, edit, detach, dissociate and delete related records without leaving the resource's Edit or View page.

Select & checkbox list - choose from existing records or create a new one

These are compatible with `BelongsTo`, `MorphTo` and `BelongsToMany` relationships.

Using a select, users will be able to choose from a list of existing records. You may also add a button that allows you to create a new record inside a modal, without leaving the page.

When using a BelongsToMany relationship with a select, you'll be able to select multiple options, not just one. Records will be automatically added to your pivot table when you submit the form. If you wish, you can swap out the multi-select dropdown with a simple checkbox list. Both components work in the same way.

Repeaters - CRUD multiple related records inside the owner's form

These are compatible with `HasMany` and `MorphMany` relationships.

Repeaters are standard form components, which can render a repeatable set of fields infinitely. They can be hooked up to a relationship, so records are automatically read, created, updated, and deleted from the related table. They live inside the main form schema, and can be used inside resource pages, as well as nesting within action modals.

From a UX perspective, this solution is only suitable if your related model only has a few fields. Otherwise, the form can get very long.

Layout form components - saving form fields to a single relationship

These are compatible with `BelongsTo`, `HasOne` and `MorphOne` relationships.

All layout form components (Grid, Section, Fieldset, etc.) have a relationship() method. When you use this, all fields within that layout are saved to the related model instead of the owner's model:

use Filament\Forms\Components\FileUpload;
use Filament\Forms\Components\Textarea;
use Filament\Forms\Components\TextInput;
use Filament\Schemas\Components\Fieldset;

Fieldset::make('Metadata')
    ->relationship('metadata')
    ->schema([
        TextInput::make('title'),
        Textarea::make('description'),
        FileUpload::make('image'),
    ])

In this example, the title, description and image are automatically loaded from the metadata relationship, and saved again when the form is submitted. If the metadata record does not exist, it is automatically created.

This feature is explained more in depth in the Forms documentation. Please visit that page for more information about how to use it.

Creating a relation manager

To create a relation manager, you can use the make:filament-relation-manager command:

php artisan make:filament-relation-manager CategoryResource posts title
  • CategoryResource is the name of the resource class for the owner (parent) model.
  • posts is the name of the relationship you want to manage.
  • title is the name of the attribute that will be used to identify posts.

This will create a CategoryResource/RelationManagers/PostsRelationManager.php file. This contains a class where you are able to define a form and table for your relation manager:

use Filament\Forms;
use Filament\Schemas\Schema;
use Filament\Tables;
use Filament\Tables\Table;

public function form(Schema $schema): Schema
{
    return $schema
        ->components([
            Forms\Components\TextInput::make('title')->required(),
            // ...
        ]);
}

public function table(Table $table): Table
{
    return $table
        ->columns([
            Tables\Columns\TextColumn::make('title'),
            // ...
        ]);
}

You must register the new relation manager in your resource's getRelations() method:

public static function getRelations(): array
{
    return [
        RelationManagers\PostsRelationManager::class,
    ];
}

Once a table and form have been defined for the relation manager, visit the Edit or View page of your resource to see it in action.

Customizing the relation manager's URL parameter

If you pass a key to the array returned from getRelations(), it will be used in the URL for that relation manager when switching between multiple relation managers. For example, you can pass posts to use ?relation=posts in the URL instead of a numeric array index:

public static function getRelations(): array
{
    return [
        'posts' => RelationManagers\PostsRelationManager::class,
    ];
}

Read-only mode

Relation managers are usually displayed on either the Edit or View page of a resource. On the View page, Filament will automatically hide all actions that modify the relationship, such as create, edit, and delete. We call this "read-only mode", and it is there by default to preserve the read-only behavior of the View page. However, you can disable this behavior, by overriding the isReadOnly() method on the relation manager class to return false all the time:

public function isReadOnly(): bool
{
    return false;
}

Alternatively, if you hate this functionality, you can disable it for all relation managers at once in the panel configuration:

use Filament\Panel;

public function panel(Panel $panel): Panel
{
    return $panel
        // ...
        ->readOnlyRelationManagersOnResourceViewPagesByDefault(false);
}

Unconventional inverse relationship names

For inverse relationships that do not follow Laravel's naming guidelines, you may wish to use the inverseRelationship() method on the table:

use Filament\Tables;
use Filament\Tables\Table;

public function table(Table $table): Table
{
    return $table
        ->columns([
            Tables\Columns\TextColumn::make('title'),
            // ...
        ])
        ->inverseRelationship('section'); // Since the inverse related model is `Category`, this is normally `category`, not `section`.
}

Handling soft-deletes

By default, you will not be able to interact with deleted records in the relation manager. If you'd like to add functionality to restore, force-delete and filter trashed records in your relation manager, use the --soft-deletes flag when generating the relation manager:

php artisan make:filament-relation-manager CategoryResource posts title --soft-deletes

You can find out more about soft-deleting here.

Listing related records

Related records will be listed in a table. The entire relation manager is based around this table, which contains actions to create, edit, attach / detach, associate / dissociate, and delete records.

You may use any features of the Table Builder to customize relation managers.

Listing with pivot attributes

For BelongsToMany and MorphToMany relationships, you may also add pivot table attributes. For example, if you have a TeamsRelationManager for your UserResource, and you want to add the role pivot attribute to the table, you can use:

use Filament\Tables;

public function table(Table $table): Table
{
    return $table
        ->columns([
            Tables\Columns\TextColumn::make('name'),
            Tables\Columns\TextColumn::make('role'),
        ]);
}

Please ensure that any pivot attributes are listed in the withPivot() method of the relationship and inverse relationship.

Creating related records

Creating with pivot attributes

For BelongsToMany and MorphToMany relationships, you may also add pivot table attributes. For example, if you have a TeamsRelationManager for your UserResource, and you want to add the role pivot attribute to the create form, you can use:

use Filament\Forms;
use Filament\Schemas\Schema;

public function form(Schema $schema): Schema
{
    return $schema
        ->components([
            Forms\Components\TextInput::make('name')->required(),
            Forms\Components\TextInput::make('role')->required(),
            // ...
        ]);
}

Please ensure that any pivot attributes are listed in the withPivot() method of the relationship and inverse relationship.

Customizing the CreateAction

To learn how to customize the CreateAction, including mutating the form data, changing the notification, and adding lifecycle hooks, please see the Actions documentation.

Editing related records

Editing with pivot attributes

For BelongsToMany and MorphToMany relationships, you may also edit pivot table attributes. For example, if you have a TeamsRelationManager for your UserResource, and you want to add the role pivot attribute to the edit form, you can use:

use Filament\Forms;
use Filament\Schemas\Schema;

public function form(Schema $schema): Schema
{
    return $schema
        ->components([
            Forms\Components\TextInput::make('name')->required(),
            Forms\Components\TextInput::make('role')->required(),
            // ...
        ]);
}

Please ensure that any pivot attributes are listed in the withPivot() method of the relationship and inverse relationship.

Customizing the EditAction

To learn how to customize the EditAction, including mutating the form data, changing the notification, and adding lifecycle hooks, please see the Actions documentation.

Attaching and detaching records

Filament is able to attach and detach records for BelongsToMany and MorphToMany relationships.

When generating your relation manager, you may pass the --attach flag to also add AttachAction, DetachAction and DetachBulkAction to the table:

php artisan make:filament-relation-manager CategoryResource posts title --attach

Alternatively, if you've already generated your resource, you can just add the actions to the $table arrays:

use Filament\Actions\AttachAction;
use Filament\Actions\BulkActionGroup;
use Filament\Actions\DetachAction;
use Filament\Actions\DetachBulkAction;
use Filament\Tables\Table;

public function table(Table $table): Table
{
    return $table
        ->columns([
            // ...
        ])
        ->headerActions([
            // ...
            AttachAction::make(),
        ])
        ->recordActions([
            // ...
            DetachAction::make(),
        ])
        ->toolbarActions([
            BulkActionGroup::make([
                // ...
                DetachBulkAction::make(),
            ]),
        ]);
}

Preloading the attachment modal select options

By default, as you search for a record to attach, options will load from the database via AJAX. If you wish to preload these options when the form is first loaded instead, you can use the preloadRecordSelect() method of AttachAction:

use Filament\Actions\AttachAction;

AttachAction::make()
    ->preloadRecordSelect()

Attaching with pivot attributes

When you attach record with the Attach button, you may wish to define a custom form to add pivot attributes to the relationship:

use Filament\Actions\AttachAction;
use Filament\Forms;

AttachAction::make()
    ->schema(fn (AttachAction $action): array => [
        $action->getRecordSelect(),
        Forms\Components\TextInput::make('role')->required(),
    ])

In this example, $action->getRecordSelect() returns the select field to pick the record to attach. The role text input is then saved to the pivot table's role column.

Please ensure that any pivot attributes are listed in the withPivot() method of the relationship and inverse relationship.

Scoping the options to attach

You may want to scope the options available to AttachAction:

use Filament\Actions\AttachAction;
use Illuminate\Database\Eloquent\Builder;

AttachAction::make()
    ->recordSelectOptionsQuery(fn (Builder $query) => $query->whereBelongsTo(auth()->user()))

Searching the options to attach across multiple columns

By default, the options available to AttachAction will be searched in the recordTitleAttribute() of the table. If you wish to search across multiple columns, you can use the recordSelectSearchColumns() method:

use Filament\Actions\AttachAction;

AttachAction::make()
    ->recordSelectSearchColumns(['title', 'description'])

Attaching multiple records

The multiple() method on the AttachAction component allows you to select multiple values:

use Filament\Actions\AttachAction;

AttachAction::make()
    ->multiple()

Customizing the select field in the attached modal

You may customize the select field object that is used during attachment by passing a function to the recordSelect() method:

use Filament\Actions\AttachAction;
use Filament\Forms\Components\Select;

AttachAction::make()
    ->recordSelect(
        fn (Select $select) => $select->placeholder('Select a post'),
    )

Handling duplicates

By default, you will not be allowed to attach a record more than once. This is because you must also set up a primary id column on the pivot table for this feature to work.

Please ensure that the id attribute is listed in the withPivot() method of the relationship and inverse relationship.

Finally, add the allowDuplicates() method to the table:

public function table(Table $table): Table
{
    return $table
        ->allowDuplicates();
}

Improving the performance of detach bulk actions

By default, the DetachBulkAction will load all Eloquent records into memory, before looping over them and detaching them one by one.

If you are detaching a large number of records, you may want to use the chunkSelectedRecords() method to fetch a smaller number of records at a time. This will reduce the memory usage of your application:

use Filament\Actions\DetachBulkAction;

DetachBulkAction::make()
    ->chunkSelectedRecords(250)

Filament loads Eloquent records into memory before detaching them for two reasons:

  • To allow individual records in the collection to be authorized with a model policy before detaching (using authorizeIndividualRecords('delete'), for example).
  • To ensure that model events are run when detaching records, such as the deleting and deleted events in a model observer.

If you do not require individual record policy authorization and model events, you can use the fetchSelectedRecords(false) method, which will not fetch the records into memory before detaching them, and instead will detach them in a single query:

use Filament\Actions\DetachBulkAction;

DetachBulkAction::make()
    ->fetchSelectedRecords(false)

Associating and dissociating records

Filament is able to associate and dissociate records for HasMany and MorphMany relationships.

When generating your relation manager, you may pass the --associate flag to also add AssociateAction, DissociateAction and DissociateBulkAction to the table:

php artisan make:filament-relation-manager CategoryResource posts title --associate

Alternatively, if you've already generated your resource, you can just add the actions to the $table arrays:

use Filament\Actions\AssociateAction;
use Filament\Actions\BulkActionGroup;
use Filament\Actions\DissociateAction;
use Filament\Actions\DissociateBulkAction;
use Filament\Tables\Table;

public function table(Table $table): Table
{
    return $table
        ->columns([
            // ...
        ])
        ->headerActions([
            // ...
            AssociateAction::make(),
        ])
        ->recordActions([
            // ...
            DissociateAction::make(),
        ])
        ->toolbarActions([
            BulkActionGroup::make([
                // ...
                DissociateBulkAction::make(),
            ]),
        ]);
}

Preloading the associate modal select options

By default, as you search for a record to associate, options will load from the database via AJAX. If you wish to preload these options when the form is first loaded instead, you can use the preloadRecordSelect() method of AssociateAction:

use Filament\Actions\AssociateAction;

AssociateAction::make()
    ->preloadRecordSelect()

Scoping the options to associate

You may want to scope the options available to AssociateAction:

use Filament\Actions\AssociateAction;
use Illuminate\Database\Eloquent\Builder;

AssociateAction::make()
    ->recordSelectOptionsQuery(fn (Builder $query) => $query->whereBelongsTo(auth()->user()))

Searching the options to associate across multiple columns

By default, the options available to AssociateAction will be searched in the recordTitleAttribute() of the table. If you wish to search across multiple columns, you can use the recordSelectSearchColumns() method:

use Filament\Actions\AssociateAction;

AssociateAction::make()
    ->recordSelectSearchColumns(['title', 'description'])

Associating multiple records

The multiple() method on the AssociateAction component allows you to select multiple values:

use Filament\Actions\AssociateAction;

AssociateAction::make()
    ->multiple()

Customizing the select field in the associate modal

You may customize the select field object that is used during association by passing a function to the recordSelect() method:

use Filament\Actions\AssociateAction;
use Filament\Forms\Components\Select;

AssociateAction::make()
    ->recordSelect(
        fn (Select $select) => $select->placeholder('Select a post'),
    )

Improving the performance of dissociate bulk actions

By default, the DissociateBulkAction will load all Eloquent records into memory, before looping over them and dissociating them one by one.

If you are dissociating a large number of records, you may want to use the chunkSelectedRecords() method to fetch a smaller number of records at a time. This will reduce the memory usage of your application:

use Filament\Actions\DissociateBulkAction;

DissociateBulkAction::make()
    ->chunkSelectedRecords(250)

Filament loads Eloquent records into memory before dissociating them for two reasons:

  • To allow individual records in the collection to be authorized with a model policy before dissociation (using authorizeIndividualRecords('update'), for example).
  • To ensure that model events are run when dissociating records, such as the updating and updated events in a model observer.

If you do not require individual record policy authorization and model events, you can use the fetchSelectedRecords(false) method, which will not fetch the records into memory before dissociating them, and instead will dissociate them in a single query:

use Filament\Actions\DissociateBulkAction;

DissociateBulkAction::make()
    ->fetchSelectedRecords(false)

Viewing related records

When generating your relation manager, you may pass the --view flag to also add a ViewAction to the table:

php artisan make:filament-relation-manager CategoryResource posts title --view

Alternatively, if you've already generated your relation manager, you can just add the ViewAction to the $table->recordActions() array:

use Filament\Actions\ViewAction;
use Filament\Tables\Table;

public function table(Table $table): Table
{
    return $table
        ->columns([
            // ...
        ])
        ->recordActions([
            ViewAction::make(),
            // ...
        ]);
}

Deleting related records

By default, you will not be able to interact with deleted records in the relation manager. If you'd like to add functionality to restore, force-delete and filter trashed records in your relation manager, use the --soft-deletes flag when generating the relation manager:

php artisan make:filament-relation-manager CategoryResource posts title --soft-deletes

Alternatively, you may add soft-deleting functionality to an existing relation manager:

use Filament\Actions\DeleteAction;
use Filament\Actions\DeleteBulkAction;
use Filament\Actions\ForceDeleteAction;
use Filament\Actions\ForceDeleteBulkAction;
use Filament\Actions\RestoreAction;
use Filament\Actions\RestoreBulkAction;
use Filament\Tables\Filters\TrashedFilter;
use Filament\Tables\Table;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\SoftDeletingScope;

public function table(Table $table): Table
{
    return $table
        ->modifyQueryUsing(fn (Builder $query) => $query->withoutGlobalScopes([
            SoftDeletingScope::class,
        ]))
        ->columns([
            // ...
        ])
        ->filters([
            TrashedFilter::make(),
            // ...
        ])
        ->recordActions([
            DeleteAction::make(),
            ForceDeleteAction::make(),
            RestoreAction::make(),
            // ...
        ])
        ->toolbarActions([
            BulkActionGroup::make([
                DeleteBulkAction::make(),
                ForceDeleteBulkAction::make(),
                RestoreBulkAction::make(),
                // ...
            ]),
        ]);
}

Customizing the DeleteAction

To learn how to customize the DeleteAction, including changing the notification and adding lifecycle hooks, please see the Actions documentation.

Importing related records

The ImportAction can be added to the header of a relation manager to import records. In this case, you probably want to tell the importer which owner these new records belong to. You can use import options to pass through the ID of the owner record:

ImportAction::make()
    ->importer(ProductImporter::class)
    ->options(['categoryId' => $this->getOwnerRecord()->getKey()])

Now, in the importer class, you can associate the owner in a one-to-many relationship with the imported record:

public function resolveRecord(): ?Product
{
    $product = Product::firstOrNew([
        'sku' => $this->data['sku'],
    ]);
    
    $product->category()->associate($this->options['categoryId']);
    
    return $product;
}

Alternatively, you can attach the record in a many-to-many relationship using the afterSave() hook of the importer:

protected function afterSave(): void
{
    $this->record->categories()->syncWithoutDetaching([$this->options['categoryId']]);
}

Accessing the relationship's owner record

Relation managers are Livewire components. When they are first loaded, the owner record (the Eloquent record which serves as a parent - the main resource model) is saved into a property. You can read this property using:

$this->getOwnerRecord()

However, if you're inside a static method like form() or table(), $this isn't accessible. So, you may use a callback to access the $livewire instance:

use Filament\Forms;
use Filament\Resources\RelationManagers\RelationManager;
use Filament\Schemas\Schema;

public function form(Schema $schema): Schema
{
    return $schema
        ->components([
            Forms\Components\Select::make('store_id')
                ->options(function (RelationManager $livewire): array {
                    return $livewire->getOwnerRecord()->stores()
                        ->pluck('name', 'id')
                        ->toArray();
                }),
            // ...
        ]);
}

All methods in Filament accept a callback which you can access $livewire->ownerRecord in.

Grouping relation managers

You may choose to group relation managers together into one tab. To do this, you may wrap multiple managers in a RelationGroup object, with a label:

use Filament\Resources\RelationManagers\RelationGroup;

public static function getRelations(): array
{
    return [
        // ...
        RelationGroup::make('Contacts', [
            RelationManagers\IndividualsRelationManager::class,
            RelationManagers\OrganizationsRelationManager::class,
        ]),
        // ...
    ];
}

Conditionally showing relation managers

By default, relation managers will be visible if the viewAny() method for the related model policy returns true.

You may use the canViewForRecord() method to determine if the relation manager should be visible for a specific owner record and page:

use Illuminate\Database\Eloquent\Model;

public static function canViewForRecord(Model $ownerRecord, string $pageClass): bool
{
    return $ownerRecord->status === Status::Draft;
}

Combining the relation manager tabs with the form

On the Edit or View page class, override the hasCombinedRelationManagerTabsWithContent() method:

public function hasCombinedRelationManagerTabsWithContent(): bool
{
    return true;
}

Customizing the content tab

On the Edit or View page class, override the getContentTabComponent() method, and use any Tab customization methods:

use Filament\Schemas\Components\Tabs\Tab;

public function getContentTabComponent(): Tab
{
    return Tab::make('Settings')
        ->icon('heroicon-m-cog');
}

Setting the position of the form tab

By default, the form tab is rendered before the relation tabs. To render it after, you can override the getContentTabPosition() method on the Edit or View page class:

use Filament\Resources\Pages\Enums\ContentTabPosition;

public function getContentTabPosition(): ?ContentTabPosition
{
    return ContentTabPosition::After;
}

Customizing relation manager tabs

To customize the tab for a relation manager, override the getTabComponent() method, and use any Tab customization methods:

use Filament\Schemas\Components\Tabs\Tab;
use Illuminate\Database\Eloquent\Model;

public static function getTabComponent(Model $ownerRecord, string $pageClass): Tab
{
    return Tab::make('Blog posts')
        ->badge($ownerRecord->posts()->count())
        ->badgeColor('info')
        ->badgeTooltip('The number of posts in this category')
        ->icon('heroicon-m-document-text');
}

If you are using a relation group, you can use the tab() method:

use Filament\Resources\RelationManagers\RelationGroup;
use Filament\Schemas\Components\Tabs\Tab;
use Illuminate\Database\Eloquent\Model;

RelationGroup::make('Contacts', [
    // ...
])
    ->tab(fn (Model $ownerRecord): Tab => Tab::make('Blog posts')
        ->badge($ownerRecord->posts()->count())
        ->badgeColor('info')
        ->badgeTooltip('The number of posts in this category')
        ->icon('heroicon-m-document-text'));

Sharing a resource's form and table with a relation manager

You may decide that you want a resource's form and table to be identical to a relation manager's, and subsequently want to reuse the code you previously wrote. This is easy, by calling the form() and table() methods of the resource from the relation manager:

use App\Filament\Resources\Blog\Posts\PostResource;
use Filament\Schemas\Schema;
use Filament\Tables\Table;

public function form(Schema $schema): Schema
{
    return PostResource::form($schema);
}

public function table(Table $table): Table
{
    return PostResource::table($table);
}

Hiding a shared form component on the relation manager

If you're sharing a form component from the resource with the relation manager, you may want to hide it on the relation manager. This is especially useful if you want to hide a Select field for the owner record in the relation manager, since Filament will handle this for you anyway. To do this, you may use the hiddenOn() method, passing the name of the relation manager:

use App\Filament\Resources\Blog\Posts\PostResource\RelationManagers\CommentsRelationManager;
use Filament\Forms\Components\Select;

Select::make('post_id')
    ->relationship('post', 'title')
    ->hiddenOn(CommentsRelationManager::class)

Hiding a shared table column on the relation manager

If you're sharing a table column from the resource with the relation manager, you may want to hide it on the relation manager. This is especially useful if you want to hide a column for the owner record in the relation manager, since this is not appropriate when the owner record is already listed above the relation manager. To do this, you may use the hiddenOn() method, passing the name of the relation manager:

use App\Filament\Resources\Blog\Posts\PostResource\RelationManagers\CommentsRelationManager;
use Filament\Tables\Columns\TextColumn;

TextColumn::make('post.title')
    ->hiddenOn(CommentsRelationManager::class)

Hiding a shared table filter on the relation manager

If you're sharing a table filter from the resource with the relation manager, you may want to hide it on the relation manager. This is especially useful if you want to hide a filter for the owner record in the relation manager, since this is not appropriate when the table is already filtered by the owner record. To do this, you may use the hiddenOn() method, passing the name of the relation manager:

use App\Filament\Resources\Blog\Posts\PostResource\RelationManagers\CommentsRelationManager;
use Filament\Tables\Filters\SelectFilter;

SelectFilter::make('post')
    ->relationship('post', 'title')
    ->hiddenOn(CommentsRelationManager::class)

Overriding shared configuration on the relation manager

Any configuration that you make inside the resource can be overwritten on the relation manager. For example, if you wanted to disable pagination on the relation manager's inherited table but not the resource itself:

use App\Filament\Resources\Blog\Posts\PostResource;
use Filament\Tables\Table;

public function table(Table $table): Table
{
    return PostResource::table($table)
        ->paginated(false);
}

It is probably also useful to provide extra configuration on the relation manager if you wanted to add a header action to create, attach, or associate records in the relation manager:

use App\Filament\Resources\Blog\Posts\PostResource;
use Filament\Actions\AttachAction;
use Filament\Actions\CreateAction;
use Filament\Tables\Table;

public function table(Table $table): Table
{
    return PostResource::table($table)
        ->headerActions([
            CreateAction::make(),
            AttachAction::make(),
        ]);
}

Customizing the relation manager Eloquent query

You can apply your own query constraints or model scopes that affect the entire relation manager. To do this, you can pass a function to the modifyQueryUsing() method of the table, inside which you can customize the query:

use Filament\Tables;
use Illuminate\Database\Eloquent\Builder;

public function table(Table $table): Table
{
    return $table
        ->modifyQueryUsing(fn (Builder $query) => $query->where('is_active', true))
        ->columns([
            // ...
        ]);
}

Customizing the relation manager title

To set the title of the relation manager, you can use the $title property on the relation manager class:

protected static ?string $title = 'Posts';

To set the title of the relation manager dynamically, you can override the getTitle() method on the relation manager class:

use Illuminate\Database\Eloquent\Model;

public static function getTitle(Model $ownerRecord, string $pageClass): string
{
    return __('relation-managers.posts.title');
}

The title will be reflected in the heading of the table, as well as the relation manager tab if there is more than one. If you want to customize the table heading independently, you can still use the $table->heading() method:

use Filament\Tables;

public function table(Table $table): Table
{
    return $table
        ->heading('Posts')
        ->columns([
            // ...
        ]);
}

Customizing the relation manager record title

The relation manager uses the concept of a "record title attribute" to determine which attribute of the related model should be used to identify it. When creating a relation manager, this attribute is passed as the third argument to the make:filament-relation-manager command:

php artisan make:filament-relation-manager CategoryResource posts title

In this example, the title attribute of the Post model will be used to identify a post in the relation manager.

This is mainly used by the action classes. For instance, when you attach or associate a record, the titles will be listed in the select field. When you edit, view or delete a record, the title will be used in the header of the modal.

In some cases, you may want to concatenate multiple attributes together to form a title. You can do this by replacing the recordTitleAttribute() configuration method with recordTitle(), passing a function that transforms a model into a title:

use App\Models\Post;
use Filament\Tables\Table;

public function table(Table $table): Table
{
    return $table
        ->recordTitle(fn (Post $record): string => "{$record->title} ({$record->id})")
        ->columns([
            // ...
        ]);
}

If you're using recordTitle(), and you have an associate action or attach action, you will also want to specify search columns for those actions:

use Filament\Actions\AssociateAction;
use Filament\Actions\AttachAction;

AssociateAction::make()
    ->recordSelectSearchColumns(['title', 'id']);

AttachAction::make()
    ->recordSelectSearchColumns(['title', 'id'])

Relation pages

Using a ManageRelatedRecords page is an alternative to using a relation manager, if you want to keep the functionality of managing a relationship separate from editing or viewing the owner record.

This feature is ideal if you are using resource sub-navigation, as you are easily able to switch between the View or Edit page and the relation page.

To create a relation page, you should use the make:filament-page command:

php artisan make:filament-page ManageCustomerAddresses --resource=CustomerResource --type=ManageRelatedRecords

When you run this command, you will be asked a series of questions to customize the page, for example, the name of the relationship and its title attribute.

You must register this new page in your resource's getPages() method:

public static function getPages(): array
{
    return [
        'index' => Pages\ListCustomers::route('/'),
        'create' => Pages\CreateCustomer::route('/create'),
        'view' => Pages\ViewCustomer::route('/{record}'),
        'edit' => Pages\EditCustomer::route('/{record}/edit'),
        'addresses' => Pages\ManageCustomerAddresses::route('/{record}/addresses'),
    ];
}
When using a relation page, you do not need to generate a relation manager with `make:filament-relation-manager`, and you do not need to register it in the `getRelations()` method of the resource.

Now, you can customize the page in exactly the same way as a relation manager, with the same table() and form().

Adding relation pages to resource sub-navigation

If you're using resource sub-navigation, you can register this page as normal in getRecordSubNavigation() of the resource:

use App\Filament\Resources\Customers\Pages;
use Filament\Resources\Pages\Page;

public static function getRecordSubNavigation(Page $page): array
{
    return $page->generateNavigationItems([
        // ...
        Pages\ManageCustomerAddresses::class,
    ]);
}

Passing properties to relation managers

When registering a relation manager in a resource, you can use the make() method to pass an array of Livewire properties to it:

use App\Filament\Resources\Blog\Posts\PostResource\RelationManagers\CommentsRelationManager;

public static function getRelations(): array
{
    return [
        CommentsRelationManager::make([
            'status' => 'approved',
        ]),
    ];
}

This array of properties gets mapped to public Livewire properties on the relation manager class:

use Filament\Resources\RelationManagers\RelationManager;

class CommentsRelationManager extends RelationManager
{
    public string $status;

    // ...
}

Now, you can access the status in the relation manager class using $this->status.

Disabling lazy loading

By default, relation managers are lazy-loaded. This means that they will only be loaded when they are visible on the page.

To disable this behavior, you may override the $isLazy property on the relation manager class:

protected static bool $isLazy = false;


title: Nested resources

Overview

Relation managers and relation pages provide you with an easy way to render a table of related records inside a resource.

For example, in a CourseResource, you may have a relation manager or page for lessons that belong to that course. You can create and edit lessons from the table, which opens modal dialogs.

However, lessons may be too complex to be created and edited in a modal. You may wish that lessons had their own resource, so that creating and editing them would be a full page experience. This is a nested resource.

Creating a nested resource

To create a nested resource, you can use the make:filament-resource command with the --nested option:

php artisan make:filament-resource Lesson --nested

To access the nested resource, you will also need a relation manager or relation page. This is where the user can see the list of related records, and click links to the "create" and "edit" pages.

To create a relation manager or page, you can use the make:filament-relation-manager or make:filament-page command:

php artisan make:filament-relation-manager CourseResource lessons title

php artisan make:filament-page ManageCourseLessons --resource=CourseResource --type=ManageRelatedRecords

When creating a relation manager or page, Filament will ask if you want each table row to link to a resource instead of opening a modal, to which you should answer "yes" and select the nested resource that you just created.

After generating the relation manager or page, it will have a property pointing to the nested resource:

use App\Filament\Resources\Courses\Resources\Lessons\LessonResource;

protected static ?string $relatedResource = LessonResource::class;

The nested resource class will have a property pointing to the parent resource:

use App\Filament\Resources\Courses\CourseResource;

protected static ?string $parentResource = CourseResource::class;

Customizing the relationship names

In the same way that relation managers and pages predict the name of relationships based on the models in those relationships, nested resources do the same. Sometimes, you may have a relationship that does not fit the traditional relationship naming convention, and you will need to inform Filament of the correct relationship names for the nested resource.

To customize the relationship names, first remove the $parentResource property from the nested resource class. Then define a getParentResourceRegistration() method:

use App\Filament\Resources\Courses\CourseResource;
use Filament\Resources\ParentResourceRegistration;

public static function getParentResourceRegistration(): ?ParentResourceRegistration
{
    return CourseResource::asParent()
        ->relationship('lessons')
        ->inverseRelationship('course');
}

You can omit the calls to relationship() and inverseRelationship() if you want to use the default names.

Registering a relation manager with the correct URL

When dealing with a nested resource that is listed by a relation manager, and the relation manager is amongst others on that page, you may notice that the URL to it is not correct when you redirect from the nested resource back to it. This is because each relation manager registered on a resource is assigned an integer, which is used to identify it in the URL when switching between multiple relation managers. For example, ?relation=0 might represent one relation manager in the URL, and ?relation=1 might represent another.

When redirecting from a nested resource back to a relation manager, Filament will assume that the relationship name is used to identify that relation manager in the URL. For example, if you have a nested LessonResource and a LessonsRelationManager, the relationship name is lessons, and should be used as the URL parameter key for that relation manager when it is registered:

public static function getRelations(): array
{
    return [
        'lessons' => LessonsRelationManager::class,
    ];
}


title: Singular resources

Overview

Resources aren't the only way to interact with Eloquent records in a Filament panel. Even though resources may solve many of your requirements, the "index" (root) page of a resource contains a table with a list of records in that resource.

Sometimes there is no need for a table that lists records in a resource. There is only a single record that the user interacts with. If it doesn't yet exist when the user visits the page, it gets created when the form is first submitted by the user to save it. If the record already exists, it is loaded into the form when the page is first loaded, and updated when the form is submitted.

For example, a CMS might have a Page Eloquent model and a PageResource, but you may also want to create a singular page outside the PageResource for editing the "homepage" of the website. This allows the user to directly edit the homepage without having to navigate to the PageResource and find the homepage record in the table.

Other examples of this include a "Settings" page, or a "Profile" page for the currently logged-in user. For these use cases, though, we recommend that you use the Spatie Settings plugin and the Profile features of Filament, which require less code to implement.

Creating a singular resource

Although there is no specific "singular resource" feature in Filament, it is a highly-requested behavior and can be implemented quite simply using a custom page with a form. This guide will explain how to do this.

Firstly, create a custom page:

php artisan make:filament-page ManageHomepage

This command will create two files - a page class in the /Filament/Pages directory of your resource directory, and a Blade view in the /filament/pages directory of the resource views directory.

The page class should contain the following elements:

  • A $data property, which will hold the current state of the form.
  • A mount() method, which will load the current record from the database and fill the form with its data. If the record doesn't exist, null will be passed to the fill() method of the form, which will assign any default values to the form fields.
  • A form() method, which will define the form schema. The form contains fields in the components() method. The record() method should be used to specify the record that the form should load relationship data from. The statePath() method should be used to specify the name of the property ($data) where the form's state should be stored.
  • A save() method, which will save the form data to the database. The getState() method runs form validation and returns valid form data. This method should check if the record already exists, and if not, create a new one. The wasRecentlyCreated property of the model can be used to determine if the record was just created, and if so then any relationships should be saved as well. A notification is sent to the user to confirm that the record was saved.
  • A getRecord() method, while not strictly necessary, is a good idea to have. This method will return the Eloquent record that the form is editing. It can be used across the other methods to avoid code duplication.
namespace App\Filament\Pages;

use App\Models\WebsitePage;
use Filament\Actions\Action;
use Filament\Forms\Components\RichEditor;
use Filament\Forms\Components\TextInput;
use Filament\Notifications\Notification;
use Filament\Pages\Page;
use Filament\Schemas\Components\Actions;
use Filament\Schemas\Components\Form;
use Filament\Schemas\Schema;

/**
 * @property-read Schema $form
 */
class ManageHomepage extends Page
{
    protected string $view = 'filament.pages.manage-homepage';

    /**
     * @var array<string, mixed> | null
     */
    public ?array $data = [];

    public function mount(): void
    {
        $this->form->fill($this->getRecord()?->attributesToArray());
    }

    public function form(Schema $schema): Schema
    {
        return $schema
            ->components([
                Form::make([
                    TextInput::make('title')
                        ->required()
                        ->maxLength(255),
                    RichEditor::make('content'),
                    // ...
                ])
                    ->livewireSubmitHandler('save')
                    ->footer([
                        Actions::make([
                            Action::make('save')
                                ->submit('save')
                                ->keyBindings(['mod+s']),
                        ]),
                    ]),
            ])
            ->record($this->getRecord())
            ->statePath('data');
    }

    public function save(): void
    {
        $data = $this->form->getState();
        
        $record = $this->getRecord();
        
        if (! $record) {
            $record = new WebsitePage();
            $record->is_homepage = true;
        }
        
        $record->fill($data);
        $record->save();
        
        if ($record->wasRecentlyCreated) {
            $this->form->record($record)->saveRelationships();
        }

        Notification::make()
            ->success()
            ->title('Saved')
            ->send();
    }
    
    public function getRecord(): ?WebsitePage
    {
        return WebsitePage::query()
            ->where('is_homepage', true)
            ->first();
    }
}

The page Blade view should render the form:

<x-filament::page>
    {{ $this->form }}
</x-filament::page>

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment