| title |
|---|
Overview |
import Aside from "@components/Aside.astro"
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.
To create a resource for the App\Models\Customer model:
php artisan make:filament-resource CustomerThis 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.
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 --simpleYour 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.
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 --generateBy 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-deletesYou can find out more about soft-deleting here.
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 --viewBy 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\\ModelsIn 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.
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 --factoryA $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 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(),
// ...
]);
}
```
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 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(),
]),
]);
}
```
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');
}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');
}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;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');
}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';
}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;
}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');
}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');
}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/customersYou 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/createSome 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/1In this example, $customer can be an Eloquent model object, or an ID.
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=1Or 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=createIf 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');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);
}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.
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';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:
- View customer, a
ViewRecordpage that provides a read-only view of the customer's details. - Edit customer, an
EditRecordpage that allows the user to edit the customer's details. - Edit customer contact, an
EditRecordpage that allows the user to edit the customer's contact details. You can learn how to create more than one Edit page. - Manage addresses, a
ManageRelatedRecordspage that allows the user to manage the customer's addresses. - Manage payments, a
ManageRelatedRecordspage that allows the user to manage the customer's payments.
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.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;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.
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 thedeleteAny()method because iterating through multiple records and checking thedelete()policy is not very performant. When using aDeleteBulkAction, if you want to call thedelete()method for each record anyway, you should use theDeleteBulkAction::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 theforceDeleteAny()method because iterating through multiple records and checking theforceDelete()policy is not very performant. When using aForceDeleteBulkAction, if you want to call theforceDelete()method for each record anyway, you should use theForceDeleteBulkAction::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 therestoreAny()method because iterating through multiple records and checking therestore()policy is not very performant. When using aRestoreBulkAction, if you want to call therestore()method for each record anyway, you should use theRestoreBulkAction::make()->authorizeIndividualRecords()method. Any records that fail the authorization check will not be processed.reorder()is used to control reordering records in a table.
If you'd like to skip authorization for a resource, you may set the $shouldSkipAuthorization property to true:
protected static bool $shouldSkipAuthorization = true;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.
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.
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)),
];
}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)),
];
}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)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())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')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'])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';
}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.
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());
}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.
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>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.
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.
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');
}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;
}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;
}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;
}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.
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.
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.
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;
}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(),
]),
// ...
];
}
}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.
"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.
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 [];
}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.
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>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.
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.
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.
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');
}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;
}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.
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.
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.
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.
"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.
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 [];
}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=EditRecordYou 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([
// ...
]);
}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,
]);
}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.
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>To create a new resource with a View page, you can use the --view flag:
php artisan make:filament-resource User --viewBy 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.
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=ViewRecordYou 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'),
];
}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(),
// ...
]);
}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.
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.
}
}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.
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=ViewRecordYou 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([
// ...
]);
}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.
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,
]);
}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.
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>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-deletesAlternatively, 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(),
// ...
];
}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(),
]);
}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.
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.
import Aside from "@components/Aside.astro"
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.
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.
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 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.
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.
To create a relation manager, you can use the make:filament-relation-manager command:
php artisan make:filament-relation-manager CategoryResource posts titleCategoryResourceis the name of the resource class for the owner (parent) model.postsis the name of the relationship you want to manage.titleis 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.
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,
];
}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);
}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`.
}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-deletesYou can find out more about soft-deleting here.
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.
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.
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.
To learn how to customize the CreateAction, including mutating the form data, changing the notification, and adding lifecycle hooks, please see the Actions documentation.
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.
To learn how to customize the EditAction, including mutating the form data, changing the notification, and adding lifecycle hooks, please see the Actions documentation.
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 --attachAlternatively, 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(),
]),
]);
}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()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.
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()))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'])The multiple() method on the AttachAction component allows you to select multiple values:
use Filament\Actions\AttachAction;
AttachAction::make()
->multiple()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'),
)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();
}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
deletinganddeletedevents 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)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 --associateAlternatively, 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(),
]),
]);
}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()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()))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'])The multiple() method on the AssociateAction component allows you to select multiple values:
use Filament\Actions\AssociateAction;
AssociateAction::make()
->multiple()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'),
)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
updatingandupdatedevents 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)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 --viewAlternatively, 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(),
// ...
]);
}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-deletesAlternatively, 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(),
// ...
]),
]);
}To learn how to customize the DeleteAction, including changing the notification and adding lifecycle hooks, please see the Actions documentation.
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']]);
}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.
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,
]),
// ...
];
}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;
}On the Edit or View page class, override the hasCombinedRelationManagerTabsWithContent() method:
public function hasCombinedRelationManagerTabsWithContent(): bool
{
return true;
}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');
}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;
}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'));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);
}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)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)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)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(),
]);
}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([
// ...
]);
}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([
// ...
]);
}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 titleIn 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'])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=ManageRelatedRecordsWhen 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'),
];
}Now, you can customize the page in exactly the same way as a relation manager, with the same table() and form().
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,
]);
}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.
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;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.
To create a nested resource, you can use the make:filament-resource command with the --nested option:
php artisan make:filament-resource Lesson --nestedTo 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=ManageRelatedRecordsWhen 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;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.
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,
];
}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.
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 ManageHomepageThis 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
$dataproperty, 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,nullwill be passed to thefill()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 thecomponents()method. Therecord()method should be used to specify the record that the form should load relationship data from. ThestatePath()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. ThegetState()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. ThewasRecentlyCreatedproperty 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>