Skip to main content

Black Friday 2025! Only until December 1st: coupon FRIDAY25 for 40% off Yearly/Lifetime membership!

Read more here
Tutorial Free

Filament v3 Nested Resources: Solution with Trait and Pages

October 12, 2023
12 min read

In Filament, you may need two-level nested resources, like Courses and Lessons inside of them. Sadly, Filament doesn't have this feature with convenient navigation out of the box yet. But in this tutorial, we will try to build nested resources functionality manually.

First, let's identify the problem we need to solve.


The Problem with Nested Resources

In short, our primary goal is to navigate to the children's records list (lessons) from the parent record (courses) list.

Also, we need to take care of quite a few things that happen next:

  • After clicking "New lesson", Filament needs to automatically assign the "parent" course and redirect back to the correct course
  • At all times, we must navigate inside the parent resource with URLs like /courses/X/lessons, /courses/X/lessons/Y, etc.

For example, if we have the Edit form of courses/1/lessons/2, we need to take care that, after submission, the user would be redirected to courses/1/lessons - the list of lessons with the course ID 1.

In other words, we need to have the parent record data at any point when navigating the child record pages to assign the correct parent and redirect to the right pages. By default, your Filament LessonResource will not know anything about the parent CourseResource.

And, as we discovered, it's not easy to override the redirects in Filament. Some functions allow to specify the property values, but others are closed from direct modifications, especially POST requests after submitting the forms.

Now, let me show our solution. Remember, it's a bit experimental, and we're happy to improve it based on your feedback!


Step 1: DB Setup

In this project, we will use a hasMany relationship.

Migrations

Schema::create('courses', function (Blueprint $table) {
$table->id();
$table->string('name');
$table->timestamps();
});
 
Schema::create('lessons', function (Blueprint $table) {
$table->id();
$table->foreignId('course_id')->constrained();
$table->string('title');
$table->text('text');
$table->timestamps();
});

Our Models define the relationship as hasMany for Courses and belongsTo for Lessons.

app/Models/Course.php

class Course extends Model
{
use HasFactory;
 
protected $fillable = [
'name'
];
 
public function lessons(): HasMany
{
return $this->hasMany(Lesson::class);
}
}

app/Models/Lesson.php

class Lesson extends Model
{
use HasFactory;
 
protected $fillable = [
'course_id',
'title',
'text',
];
 
public function course(): BelongsTo
{
return $this->belongsTo(Course::class);
}
}

That's it for our DB setup.


Step 2: Creating Two Filament Resources

For our Resource, we used the following commands:

php artisan make:filament-resource Course --generate
php artisan make:filament-resource Lesson --generate

This will create the Filament resources, generating the columns/inputs directly from the DB with the --generate flag.

For now, at this stage, they are not nested yet.

We won't change anything in the CourseResource for now.

app/Filament/Resources/CourseResource.php

class CourseResource extends Resource
{
protected static ?string $model = Course::class;
 
protected static ?string $navigationIcon = 'heroicon-o-rectangle-stack';
 
public static function form(Form $form): Form
{
return $form
->schema([
Forms\Components\TextInput::make('name')
->required()
->maxLength(255),
]);
}
 
public static function table(Table $table): Table
{
return $table
->columns([
Tables\Columns\TextColumn::make('name')
->searchable(),
Tables\Columns\TextColumn::make('created_at')
->dateTime()
->sortable()
->toggleable(isToggledHiddenByDefault: true),
Tables\Columns\TextColumn::make('updated_at')
->dateTime()
->sortable()
->toggleable(isToggledHiddenByDefault: true),
])
->filters([
//
])
->actions([
Tables\Actions\EditAction::make(),
])
->bulkActions([
Tables\Actions\BulkActionGroup::make([
Tables\Actions\DeleteBulkAction::make(),
]),
]);
}
 
public static function getRelations(): array
{
return [
//
];
}
 
public static function getPages(): array
{
return [
'index' => Pages\ListCourses::route('/'),
'create' => Pages\CreateCourse::route('/create'),
'edit' => Pages\EditCourse::route('/{record}/edit'),
];
}
}

Next, in the LessonResource, we did a few modifications:

  • Remove the course_id field from the form (we will assign it automatically)
  • Remove the course.name column from the table (we will show only lessons of a specific course)
  • Disable the navigation (lessons will be accessible only through the "Courses" menu)

app/Filament/Resources/LessonResource.php

class LessonResource extends Resource
{
protected static ?string $model = Lesson::class;
 
protected static bool $shouldRegisterNavigation = false;
 
public static function form(Form $form): Form
{
return $form
->schema([
// This field was generated, but we will not use it
Forms\Components\Select::make('course_id')
->relationship('course', 'name')
->required(),
Forms\Components\TextInput::make('title')
->required()
->maxLength(255),
Forms\Components\Textarea::make('text')
->required()
->maxLength(65535)
->columnSpanFull(),
]);
}
 
public static function table(Table $table): Table
{
return $table
->columns([
// This column was generated, but we will not use it
Tables\Columns\TextColumn::make('course.name')
->numeric()
->sortable(),
Tables\Columns\TextColumn::make('title')
->searchable(),
Tables\Columns\TextColumn::make('created_at')
->dateTime()
->sortable()
->toggleable(isToggledHiddenByDefault: true),
Tables\Columns\TextColumn::make('updated_at')
->dateTime()
->sortable()
->toggleable(isToggledHiddenByDefault: true),
])
->filters([
//
])
->actions([
Tables\Actions\EditAction::make(),
])
->bulkActions([
Tables\Actions\BulkActionGroup::make([
Tables\Actions\DeleteBulkAction::make(),
]),
]);
}
 
public static function getRelations(): array
{
return [
//
];
}
 
public static function getPages(): array
{
return [
'index' => Pages\ListLessons::route('/'),
'create' => Pages\CreateLesson::route('/create'),
'edit' => Pages\EditLesson::route('/{record}/edit'),
];
}
}

Everything else generated is the same as the default Filament Resource (Create, Edit, List, etc.).


Step 3: Implementing the Nested Resource

Here comes the main part of this tutorial. We will have to modify quite a few files, so we will discuss each file and why we changed something. Let's start with creating a new Trait:

Here's what the Trait does:

  • It will automatically resolve the parent resource's model and set it to $this->parent.
  • It will automatically apply a filter to the table query to only show records with the parent's ID.
  • It will automatically generate breadcrumbs for the parent resource and the current resource. For example: Courses > Filament Example - Nested resources > Lessons > List
  • It will automatically resolve the relationship key like course_id for the lessons table and set it to $this->relationshipKey.

Notice: there's no Artisan command make:trait. You need to create that PHP file manually in your IDE.

app/Filament/Traits/HasParentResource.php

use Exception;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\ModelNotFoundException;
 
/**
* @property string|null $relationshipKey Define custom relationship key (if it does not match the table name pattern).
* @property string|null $pageNamePrefix Define custom child page name prefix (if it does not match the resource's slug).
*/
trait HasParentResource
{
public Model|int|string|null $parent = null;
 
public function bootHasParentResource(): void
{
// Retrieve the parent resource's model.
if ($parent = (request()->route('parent') ?? request()->input('parent'))) {
$parentResource = $this->getParentResource();
 
$this->parent = $parentResource::resolveRecordRouteBinding($parent);
 
if (!$this->parent) {
throw new ModelNotFoundException();
}
}
}
 
public static function getParentResource(): string
{
$parentResource = static::getResource()::$parentResource;
 
if (!isset($parentResource)) {
throw new Exception('Parent resource is not set for '.static::class);
}
 
return $parentResource;
}
 
protected function applyFiltersToTableQuery(Builder $query): Builder
{
// Apply any filters before the parent relationship key is applied.
$query = parent::applyFiltersToTableQuery($query);
 
return $query->where($this->getParentRelationshipKey(), $this->parent->getKey());
}
 
public function getParentRelationshipKey(): string
{
// You can set Custom relationship key (if it does not match the table name pattern) via $relationshipKey property.
// Otherwise, it will be auto-resolved.
return $this->relationshipKey ?? $this->parent?->getForeignKey();
}
 
public function getChildPageNamePrefix(): string
{
return $this->pageNamePrefix ?? (string) str(static::getResource()::getSlug())
->replace('/', '.')
->afterLast('.');
}
 
public function getBreadcrumbs(): array
{
$resource = static::getResource();
$parentResource = static::getParentResource();
 
$breadcrumbs = [
$parentResource::getUrl() => $parentResource::getBreadCrumb(),
$parentResource::getRecordTitle($this->parent),
$parentResource::getUrl(name: $this->getChildPageNamePrefix() . '.index', parameters: ['parent' => $this->parent]) => $resource::getBreadCrumb(),
];
 
if (isset($this->record)) {
$breadcrumbs[] = $resource::getRecordTitle($this->record);
}
 
$breadcrumbs[] = $this->getBreadCrumb();
 
return $breadcrumbs;
}
}

Next, we will modify our CourseResource to add a few things:

  • Add record title for the parent resource to be used in breadcrumbs
  • Add a link to each row for its nested resources
  • Define routes for nested resource

app/Filament/Resources/CourseResource.php

use App\Filament\Resources\LessonResource\Pages\CreateLesson;
use App\Filament\Resources\LessonResource\Pages\EditLesson;
use App\Filament\Resources\LessonResource\Pages\ListLessons;
use Filament\Facades\Filament;
use Filament\Tables\Actions\Action;
use Illuminate\Contracts\Support\Htmlable;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Support\Facades\Route;
 
// ...
 
class CourseResource extends Resource
{
// ...
 
public static function getRecordTitle(?Model $record): string|null|Htmlable
{
return $record->name;
}
 
public static function table(Table $table): Table
{
return $table
->columns([
// ...
])
->filters([
// ...
])
->actions([
Tables\Actions\EditAction::make(),
Action::make('Manage lessons')
->color('success')
->icon('heroicon-m-academic-cap')
->url(
fn (Course $record): string => static::getUrl('lessons.index', [
'parent' => $record->id,
])
),
])
->bulkActions([
Tables\Actions\BulkActionGroup::make([
Tables\Actions\DeleteBulkAction::make(),
]),
]);
}
 
public static function getRelations(): array
{
return [
//
];
}
 
public static function getPages(): array
{
return [
'index' => Pages\ListCourses::route('/'),
'create' => Pages\CreateCourse::route('/create'),
'edit' => Pages\EditCourse::route('/{record}/edit'),
 
// Lessons
'lessons.index' => ListLessons::route('/{parent}/lessons'),
'lessons.create' => CreateLesson::route('/{parent}/lessons/create'),
'lessons.edit' => EditLesson::route('/{parent}/lessons/{record}/edit'),
];
}
}

Now that our CourseResource is ready, we can move on to our LessonResource. We will do the following:

  • Add record title for the child resource to be used in breadcrumbs
  • ADd parent resource property
  • Remove route definitions (they can stay, but it's not needed)

app/Filament/Resources/LessonResource.php

use Illuminate\Contracts\Support\Htmlable;
use Illuminate\Database\Eloquent\Model;
 
// ...
 
class LessonResource extends Resource
{
// ...
 
public static string $parentResource = CourseResource::class;
 
public static function getRecordTitle(?Model $record): string|null|Htmlable
{
return $record->title;
}
 
public static function table(Table $table): Table
{
return $table
->columns([
// ...
])
->filters([
// ...
])
->actions([
Tables\Actions\EditAction::make(),
Tables\Actions\EditAction::make()
->url(
fn (Pages\ListLessons $livewire, Model $record): string => static::$parentResource::getUrl('lessons.edit', [
'record' => $record,
'parent' => $livewire->parent,
])
),
Tables\Actions\DeleteAction::make(),
])
->bulkActions([
Tables\Actions\BulkActionGroup::make([
Tables\Actions\DeleteBulkAction::make(),
]),
]);
}
 
public static function getPages(): array
{
return [
'index' => Pages\ListLessons::route('/'),
'create' => Pages\CreateLesson::route('/create'),
'edit' => Pages\EditLesson::route('/{record}/edit'),
];
}
}

Now, we will start with our actual pages, starting with the ListLessons page. We will do the following:

  • Add the HasParentResource trait from above
  • Modify headers and create action to use the parent resource's URL

app/Filament/Resources/LessonResource/Pages/ListLessons.php

use App\Filament\Resources\CourseResource;
use App\Filament\Traits\HasParentResource;
 
// ...
 
class ListLessons extends ListRecords
{
use HasParentResource;
 
protected static string $resource = LessonResource::class;
 
protected function getHeaderActions(): array
{
return [
Actions\CreateAction::make(),
Actions\CreateAction::make()
->url(
fn (): string => static::getParentResource()::getUrl('lessons.create', [
'parent' => $this->parent,
])
),
];
}
}

That's it for our ListLessons page. Now, on the CreateLesson page, we will do the following:

  • Add the HasParentResource trait
  • Modify the redirect URL to use the parent resource's URL
  • Mutate the form data to add the parent resource's ID

app/Filament/Resources/LessonResource/Pages/CreateLesson.php

use App\Filament\Resources\CourseResource;
use App\Filament\Traits\HasParentResource;
 
// ...
 
class CreateLesson extends CreateRecord
{
use HasParentResource;
 
protected static string $resource = LessonResource::class;
 
protected function getRedirectUrl(): string
{
return $this->previousUrl ?? static::getParentResource()::getUrl('lessons.index', [
'parent' => $this->parent,
]);
}
 
// This can be moved to Trait, but we are keeping it here
// to avoid confusion in case you mutate the data yourself
protected function mutateFormDataBeforeCreate(array $data): array
{
// Set the parent relationship key to the parent resource's ID.
$data[$this->getParentRelationshipKey()] = $this->parent->id;
 
return $data;
}
}

And finally, on the EditLesson page, we will do the following:

  • Add the HasParentResource trait
  • Modify the redirect URL to use the parent resource's URL
  • Configure the delete action redirects

app/Filament/Resources/LessonResource/Pages/EditLesson.php

use App\Filament\Resources\CourseResource;
use App\Filament\Traits\HasParentResource;
 
// ...
 
class EditLesson extends EditRecord
{
use HasParentResource;
 
protected static string $resource = LessonResource::class;
 
protected function getHeaderActions(): array
{
return [
Actions\DeleteAction::make(),
];
}
 
protected function getRedirectUrl(): string
{
return $this->previousUrl ?? static::getParentResource()::getUrl('lessons.index', [
'parent' => $this->parent,
]);
}
 
protected function configureDeleteAction(Actions\DeleteAction $action): void
{
$resource = static::getResource();
 
$action->authorize($resource::canDelete($this->getRecord()))
->successRedirectUrl(static::getParentResource()::getUrl('lessons.index', [
'parent' => $this->parent,
]));
}
}

That's it! You should have working nested resources now! Here's how that looks in the admin panel:

This opens up a new page with the list of lessons for the selected course:

And here's the Edit form for the lesson:


Final Words

This is an experimental implementation of nested resources we came up with. We are considering adding this functionality to Filament core or releasing it as a plugin with a more robust solution, so any feedback is appreciated.

You can find the complete code for this tutorial on GitHub and all the changes in this commit

If you are looking for a multi-tenant example, you can find it in this GitHub Branch


Updates

The last revision of this tutorial was made on October 21, 2023. This includes a few updates:

  • We have simplified the implementation to use less code and less complexity.
    • We have improved the Trait to refer to the resource to get the parent resource.
    • There was a removal of $parentResource on our List, Create, and Edit pages.
    • Removed custom URL resolving in our Parent resource
    • Allowed Table actions to be moved back to the Resource file instead of the List file.
  • Included a tenant code example link.
    • This example has a fully working multi-tenancy support for Filament.

This came from our community member who made a PR Guilherme Saade, so big thanks to him!


If you want more Filament examples, you can find more real-life projects on our FilamentExamples.com.

Enjoyed This Tutorial?

Get access to all premium tutorials, video and text courses, and exclusive Laravel resources. Join our community of 10,000+ developers.

Comments & Discussion

HS
Heng Sivhuy ✓ Link copied!

Hello,

I'm using filament3 and looking for neseted resoure for my project.

I follow your tutorial, and when i click for Manage lessons it show me an error "Call to a member function uri() on null".

M
Modestas ✓ Link copied!

Hi, can you add more details on the error message itself? Not sure what would be wrong looking at this :)

HS
Heng Sivhuy ✓ Link copied!

Dear Mr.Modestas

I'm using filament v3 with laravel-modules, I create resoures under module.

I just download your code to test, it's work fine. So I'm not sure what happen with the code.

It error in this block code

public static function getUrl(string $name = 'index', array $parameters = [], bool $isAbsolute = true, ?string $panel = null, ?Model $tenant = null): string { $parameters['tenant'] ??= ($tenant ?? Filament::getTenant());

$routeBaseName = static::getRouteBaseName(panel: $panel);
$routeFullName = "{$routeBaseName}.{$name}";
 
 
$routePath = Route::getRoutes()->getByName($routeFullName)->uri(); //This line throw error
 
if (str($routePath)->contains('{parent}')) {
$parameters['parent'] ??= (request()->route('parent') ?? request()->input('parent'));
}
 
return route($routeFullName, $parameters, $isAbsolute);
}
M
Modestas ✓ Link copied!

It seems that you are missing something in your setup. Check what might have gone wrong there and double check that you have all the routes defined on parent resource correctly

HS
Heng Sivhuy ✓ Link copied!

yes, i will try to check.

Thank you for your kindness. :)

W
Willian ✓ Link copied!

Awesome!!! You're the best.

I have only one point, I am getting error when try to delete a lesson.

Filament\Resources\Pages\ListRecords::Filament\Resources\Pages{closure}(): Return value must be of type string, null returned

HS
Heng Sivhuy ✓ Link copied!

I just figured it out, now it work fine. In List Detail Record
// You can set the Custom relationship key (if it does not match the table name pattern) via the $relationshipKey property.
// Otherwise, it will be auto-resolved.

public function getParentRelationshipKey(): string { return $this->relationshipKey = "your_foreign_key"; }


In DetailResource

//Return string the same url you create in MasterResource

public static function getPluralModelLabel(): string { return __('details'); }

I hope it will help you!

E
egemenk ✓ Link copied!

This may also happen if your getRecordTitle function returns a null value. Remove the getRecordTitle function from your child resource to check if it solves the issue.

M
Modestas ✓ Link copied!

But that function technically accepts the null return. So it's a bit weird that it would be like this.

Then again, maybe there is another issue with the code itself.

ps. This issue is in theory not existing with the latest code changes. Or at least, we did not encounter it nor we heard it happen again!

E
egemenk ✓ Link copied!

@Modestas https://github.com/filamentphp/filament/blob/8b1b5ee045d93f2423295e8110c8bff65739110a/packages/panels/src/Resources/Pages/ListRecords.php#L271 The anonymous function on this line returns a string, which restricts the getRecordTitle function to returning only strings. This is why it fails when the function returns null.

M
Modestas ✓ Link copied!

Oh, that makes sense! I probably overlooked that there. So then this function should return a string in any case. Will see what can be done (but honestly, I did not encounter this issue myself yet)

E
egemenk ✓ Link copied!

I think this is a bug on Filament side. The anonymous function should reflect the return types of the getRecordTitle function.

MM
Mauricio Montoya ✓ Link copied!

Hey guys, don't really know what I'm missing... after do everything you did in the guide, I'm getting a Route [filament.admin.resources.variations.index] not defined.

(I'm trying to replicate this with Services and Variations )

M
Modestas ✓ Link copied!

Hi, some code or debugging log would be awesome here!

But my main concern is that some routes are not registered correctly, or there is a method missing (like get url or breadcrumbs).

ps. We will be updating this article soon with improved and reduced code, you can already check that in repository!

M
Modestas ✓ Link copied!

Updated the article with new code! Please check if that solves your issue (new implementation and removed a lot of repeating code)

MM
Mauricio Montoya ✓ Link copied!

Cool, it's working now with the new code, thanks a lot!!

MM
Mauricio Montoya ✓ Link copied!

Hi again, for some reason it's not autopopulating the Parent Relationship Key even if I put manually the foreing key inside the getParentRelationshipKey function... any ideas ? https://flareapp.io/share/omwKOqWm

M
Modestas ✓ Link copied!

It seems that you forgot to add the CreateResource code to add the parent information:

protected function mutateFormDataBeforeCreate(array $data): array
{
// Set the parent relationship key to the parent resource's ID.
$data[$this->getParentRelationshipKey()] = $this->parent->id;
 
return $data;
}
MM
Mauricio Montoya ✓ Link copied!

I have it there, but one weird thing I see is happening when I use the Create button when the table is Empty -> "emptyStateActions" and also is happening on delete button attached to each row, it's throwing an error "Filament\Resources\Pages\ListRecords::Filament\Resources\Pages{closure}(): Return value must be of type string, null returned" but when I use the delete button inside the record, it works fine...

M
Modestas ✓ Link copied!

I think you have some issues with the code on your end.

I just tries to add empty state action:

->emptyStateActions([
Action::make('create')
->label('Create post')
->url(function (Pages\ListLessons $livewire) {
return static::$parentResource::getUrl('lessons.create', [
'parent' => $livewire->parent,
]);
})
->icon('heroicon-m-plus')
->button(),
]);

And it worked perfectly. Same with Delete button - it did work on both inside the table and inside the records.

Sadly, withou the actual code - there is not much I can help you as it is working just fine on our end...

MM
Mauricio Montoya ✓ Link copied!

don't know what's happening, I just put your code and this happened

https://flareapp.io/share/LPlK1bnm

M
Modestas ✓ Link copied!

Your namespace seems to be incorrect. If you don't know the exact one it should be - just remove yhe property type (pages\CreateVariation) and leave only the variable livewire

MM
Mauricio Montoya ✓ Link copied!

perfect, now is working! thank you!!

PN
Phanith Noch ✓ Link copied!

It is working fine for 2 levels of nested resources, but I'm encountering errors when attempting to add a third level of nested resources. Is it possible to make it work with 3 levels using your solutions?

Course 2. Lessons 3. Videos I'm receiving an error stating 'Missing required parameter for [Route: filament.admin.resources.lessons.videos.index] [URI: admin/lessons/{parent}/videos] [Missing parameter: parent].'

M
Modestas ✓ Link copied!

currently this is not possible. it was designed for 2 levels only, as 3 and more will require a lot more code modifications.

Sorry

PN
Phanith Noch ✓ Link copied!

Anyway, thank you for your hard work.

ND
Nils Domin ✓ Link copied!

@Phanith Could you solve this for 3 levels?

HR
Hani Ramadhan ✓ Link copied!

@Nils Domin, I can create unlimited nested resources in filament using nested resource filament plugin by Guava.

additionally, I just simply modify its breadcrumbs function to match my need. but the plugin itself is great and working.

DA
davide alpi ✓ Link copied!

@Hani Ramadhan

I believe I'm on your same track. Would you mind sharing how you modified its breadcrumbs function?

SZ
Sam Zhong ✓ Link copied!

Hi, I found that the filter is no longer working after use the nested resource. Tested out a while and found this block of code is the the problem:

protected function applyFiltersToTableQuery(Builder $query): Builder
{
return $query->where($this->getParentRelationshipKey(), $this->parent->getKey());
}

Seems like it by pass the parent's method. Fixed the filter issue by adding parent call:

protected function applyFiltersToTableQuery(Builder $query): Builder
{
$query = parent::applyFiltersToTableQuery($query);
return $query->where($this->getParentRelationshipKey(), $this->parent->getKey());
}
M
Modestas ✓ Link copied!

Good catch! Thank you, updating the example!

IM
Ismail Mahmoud ✓ Link copied!

when i tried to add Tables\Actions\ViewAction::make(), to the table, i got the error message:

error: Filament\Resources\Pages\ListRecords::Filament\Resources\Pages{closure}(): Return value must be of type string, null returned

here is my resource code:

<?php
 
namespace App\Filament\Resources;
 
use App\Filament\Resources\CourseResource\Pages;
use App\Filament\Resources\CourseResource\RelationManagers;
use App\Models\Course;
use Filament\Forms;
use Filament\Forms\Form;
use Filament\Resources\Resource;
use Filament\Tables;
use Filament\Tables\Table;
use Filament\Forms\Components\ColorPicker;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\SoftDeletingScope;
// add course lessons
use App\Filament\Resources\LessonResource\Pages\CreateLesson;
use App\Filament\Resources\LessonResource\Pages\EditLesson;
use App\Filament\Resources\LessonResource\Pages\ListLessons;
use Filament\Facades\Filament;
use Filament\Tables\Actions\Action;
use Illuminate\Contracts\Support\Htmlable;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Support\Facades\Route;
use Illuminate\Support\Str;
use Filament\Forms\Set;
use Filament\Forms\Components\RichEditor;
 
 
class CourseResource extends Resource
{
protected static ?string $model = Course::class;
 
protected static ?string $navigationIcon = 'heroicon-o-rectangle-stack';
 
public static function getRecordTitle(?Model $record): string|null|Htmlable
{
return $record->name;
}
 
public static function form(Form $form): Form
{
return $form
->schema([
Forms\Components\TextInput::make('title')
->required()
->reactive()
->afterStateUpdated(fn (Set $set, ?string $state) => $set('slug', Str::slug($state))),
Forms\Components\TextInput::make('slug')->required()->readOnly(),
Forms\Components\RichEditor::make('description')
->columnSpanFull(),
Forms\Components\TextInput::make('old_price')
// ->lt('new_price')
->numeric(),
Forms\Components\TextInput::make('new_price')
->lt('old_price')
->numeric(),
Forms\Components\Select::make('category_id')
->relationship('category', 'name')
->required(),
Forms\Components\ColorPicker::make('color'),
Forms\Components\Toggle::make('published')->default(true),
]);
}
 
public static function table(Table $table): Table
{
return $table
->columns([
Tables\Columns\TextColumn::make('title')
->searchable(),
Tables\Columns\TextColumn::make('slug')
->searchable(),
Tables\Columns\IconColumn::make('published')
->boolean(),
........
])
->filters([
//
])
->actions([
Tables\Actions\ViewAction::make(),
 
Tables\Actions\EditAction::make(),
 
Action::make('Manage lessons')
->color('success')
->icon('heroicon-m-academic-cap')
->url(
fn (Course $record): string => static::getUrl('lessons.index', [
'parent' => $record->id,
])
),
])
->bulkActions([
Tables\Actions\BulkActionGroup::make([
Tables\Actions\DeleteBulkAction::make(),
]),
]);
}
 
public static function getRelations(): array
{
return [
//
];
}
 
public static function getPages(): array
{
return [
'index' => Pages\ListCourses::route('/'),
'create' => Pages\CreateCourse::route('/create'),
'edit' => Pages\EditCourse::route('/{record}/edit'),
 
// Lessons
'lessons.index' => ListLessons::route('/{parent}/lessons'),
'lessons.create' => CreateLesson::route('/{parent}/lessons/create'),
'lessons.edit' => EditLesson::route('/{parent}/lessons/{record}/edit'),
];
}
}
M
Modestas ✓ Link copied!

Hi, you need to add the HasParent trait to your view page too. We did not do that, but it should follow the exact same setup as we have with our List/Create/Edit pages

IM
Ismail Mahmoud ✓ Link copied!

I don't have view page, but the problem was in this line

public static function getRecordTitle(?Model $record): string|null|Htmlable
{
return $record->name;
}

the correct in my case is title instead of name.

M
Modestas ✓ Link copied!

I got confused by viewQction then. But happy you solved it!

IM
Ismail Mahmoud ✓ Link copied!

Thanks for your time

YD
Yimy David ✓ Link copied!

It also works for me, since I also generated the resource --simple

IM
Ismail Mahmoud ✓ Link copied!

How to redirect to course lessons page after create in this example?

M
Modestas ✓ Link copied!

You can modify the CreateXXX redirect:

protected function getRedirectUrl(): string
{
return $this->previousUrl ?? static::getParentResource()::getUrl('lessons.index', [
'parent' => $this->parent,
]);
}

And in there you can change where it redirects.

IM
Ismail Mahmoud ✓ Link copied!

the question in another way: After i create a course i need to redirect to the manage lessons page

M
Modestas ✓ Link copied!

So you do the redirect in create course file, to this URL

IM
Ismail Mahmoud ✓ Link copied!

It will need the id of the parent , how to get it?

IM
Ismail Mahmoud ✓ Link copied!

$this->id ???

M
Modestas ✓ Link copied!

$this->record->id

IM
Ismail Mahmoud ✓ Link copied!

Thanks

DK
Diógenes Konrad Götz ✓ Link copied!

How can I use slideOver() on child pages?

protected function getHeaderActions(): array
{
return [
Actions\CreateAction::make()
->url(
fn (): string => static::getParentResource()::getUrl('menu-sections.create', [
'parent' => $this->parent,
])
)->slideOver(),
];
}
M
Modestas ✓ Link copied!

This is not really related to the lesson here, but you need a very basic resource. That resource can't register create route in order for slideOver to work correctly. For example, on LessonResource:

public static function getPages(): array
{
return [
'index' => Pages\ListLessons::route('/'),
// 'create' => Pages\CreateLesson::route('/create'),
// 'edit' => Pages\EditLesson::route('/{record}/edit'),
];
}

Commenting out edit/create routes - makes the slide over work.

ps. Comment out the url() on your create button - it is not needed if you are using slideOvers

V
vrcorp ✓ Link copied!

Hi. i tried, but facing error like this. Trait "App\Filament\Traits\HasParentResource" not found

Filament\Resources\TaskResource\Pages\CreateTask.php : 11

this line 1-11

V
vrcorp ✓ Link copied!
<?php
namespace App\Filament\Resources\TaskResource\Pages;
use App\Filament\Resources\TaskResource;
use Filament\Actions;
use Filament\Resources\Pages\CreateRecord;
use App\Filament\Resources\ReportResource;
use App\Filament\Traits\HasParentResource;
class CreateTask extends CreateRecord //here error
V
vrcorp ✓ Link copied!
Trait "App\Filament\Traits\HasParentResource" not found #this error messages
V
vrcorp ✓ Link copied!

sorry solved, by adding

namespace App\Filament\Traits;

at first line

V
vrcorp ✓ Link copied!

but i have another error.

Call to undefined method App\Models\Report::resolveRecordRouteBinding()

This my Models

<?php
 
namespace App\Models;
 
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\HasMany;
 
class Report extends Model
{
use HasFactory;
 
protected static function booted()
{
static::creating(function ($report) {
$report->user_id = auth()->id();
});
}
public function tasks(): HasMany
{
return $this->hasMany(Task::class);
}
 
}
M
Modestas ✓ Link copied!

Can you add more details about the error you are facing? This can be many things (and probably is just a misconfiguration/syntax error somewhere)

DK
Diógenes Konrad Götz ✓ Link copied!

You must have set the model in $parentResource and not the resource.

class LessonResource extends Resource
{
// ...
 
public static string $parentResource = CourseResource::class;
K
kylemabaso ✓ Link copied!

I'm getting SQLSTATE[HY000]: General error: 1364 Field 'course_id' doesn't have a default value

<?php
 
namespace App\Filament\Resources\ModuleResource\Pages;
 
use App\Filament\Resources\ModuleResource;
use Filament\Actions;
use Filament\Resources\Pages\CreateRecord;
use App\Filament\Resources\CourseResource;
use App\Filament\Traits\HasParentResource;
 
class CreateModule extends CreateRecord
{
use HasParentResource;
 
protected static string $resource = ModuleResource::class;
 
protected function getRedirectUrl(): string
{
return $this->previousUrl ?? static::getParentResource()::getUrl('modules.index', [
'parent' => $this->parent,
]);
}
 
// This can be moved to Trait, but we are keeping it here
// to avoid confusion in case you mutate the data yourself
protected function mutateFormDataBeforeCreate(array $data): array
{
// Set the parent relationship key to the parent resource's ID.
$data[$this->getParentRelationshipKey()] = $this->parent->id;
 
 
return $data;
}
}
M
Modestas ✓ Link copied!

Please dump what the getParentRelationshipKey function returns. There is some kind of misconfiguration on your end as this is a module, and for whatever reason it takes course_id.

Or in other words - the code you added does not tell me what you were trying to do, so I can't help effectively. Sorry!

K
kylemabaso ✓ Link copied!

Hi. It seems that my problem was here:

Actions\CreateAction::make()
->url(
fn (): string => static::getParentResource()::getUrl('lessons.create', [
'parent' => $this->parent,
])
),

I copied that from the guide. I've now updated it to:

protected function getHeaderActions(): array
{
return [
Actions\CreateAction::make()
->label('Create Lesson')
->url(function (ListLessons $livewire) {
return static::$parentResource::getUrl('lessons.create', [
'parent' => $livewire->parent,
]);
})
];
}
 
The error I'm getting now is:
 
SQLSTATE[23000]: Integrity constraint violation: 1452 Cannot add or update a child row: a foreign key constraint fails (`spcorp`.`lessons`, CONSTRAINT `lessons_module_id_foreign` FOREIGN KEY (`module_id`) REFERENCES `modules` (`id`) ON DELETE CASCADE)
M
Modestas ✓ Link copied!

Do you have parentResource set on your child resources?

K
kylemabaso ✓ Link copied!

Yes

class CreateLesson extends CreateRecord
{
use HasParentResource;
 
protected static string $resource = LessonResource::class;
 
protected function getRedirectUrl(): string
{
return $this->previousUrl ?? static::getParentResource()::getUrl('lessons.index', [
'parent' => $this->parent,
]);
}
 
protected function mutateFormDataBeforeCreate(array $data): array
{
// Set the parent relationship key to the parent resource's ID.
$data[$this->getParentRelationshipKey()] = $this->parent->id;
 
return $data;
}
}
M
Modestas ✓ Link copied!

I meant on the LessonResource, not on the lesson page. We have had a few updates to this article to make it more reliable

K
kylemabaso ✓ Link copied!

I do, this is my lesson resource:

class LessonResource extends Resource
{
public static string $parentResource = ModuleResource::class;
 
protected static ?string $model = Lesson::class;
 
protected static bool $shouldRegisterNavigation = false;
 
public static function getRecordTitle(?Model $record): string|null|Htmlable
{
return $record->title;
}
 
public static function form(Form $form): Form
{
return $form
->schema([
Forms\Components\TextInput::make('sequence')
->required()
->numeric()
->default(1),
Forms\Components\TextInput::make('title')
->required()
->maxLength(255),
Forms\Components\TextInput::make('video')
->required()
->maxLength(255),
Forms\Components\RichEditor::make('synopsis')
->columnSpanFull(),
Forms\Components\TextInput::make('duration')
->nullable()
->numeric(),
]);
}
 
public static function table(Table $table): Table
{
return $table
->columns([
Tables\Columns\TextColumn::make('sequence')
->numeric()
->sortable(),
Tables\Columns\TextColumn::make('title')
->searchable(),
Tables\Columns\TextColumn::make('video')
->searchable(),
Tables\Columns\TextColumn::make('duration')
->numeric()
->sortable(),
Tables\Columns\TextColumn::make('module.title')
->numeric()
->sortable(),
])
->filters([
//
])
->actions([
Tables\Actions\EditAction::make()
->url(
fn (Pages\ListLessons $livewire, Model $record): string => static::$parentResource::getUrl('lessons.edit', [
'record' => $record,
'parent' => $livewire->parent,
])
),
Tables\Actions\DeleteAction::make(),
])
->bulkActions([
Tables\Actions\BulkActionGroup::make([
Tables\Actions\DeleteBulkAction::make(),
]),
])
->emptyStateActions([
Tables\Actions\CreateAction::make(),
]);
}
 
public static function getRelations(): array
{
return [
//
];
}
public static function getPages(): array
{
return [
'index' => Pages\ListLessons::route('/'),
'create' => Pages\CreateLesson::route('/create'),
'edit' => Pages\EditLesson::route('/{record}/edit'),
];
}
```
M
Modestas ✓ Link copied!

Hmm, in that case - there is something off with Database setup. Sorry, really hard to help here. If you want us to better look at the issue - get in touch via email and provide the repository with your code - we can take a look and see what the problem might be :)

FC
Firman Canva ✓ Link copied!

Symfony \ Component \Routing \ Exception \ RouteNotFoundException PHP 8.1.10 Laravel 10.21.0 Route [filament.app.resources.answers.index] not defined.

https://prnt.sc/FGvrbDZXG1SU

K
kylemabaso ✓ Link copied!

It seems like your route for answer should be filament.app.resources.questions.answers.index. You're missing the word question.

FC
Firman Canva ✓ Link copied!

I followed this tutorial, while I used filament tenancy, where I can fix the route

K
kylemabaso ✓ Link copied!

Well, it's hard to say because I don't know what page you're visiting to get this error. Share the code for the page.

FC
K
kylemabaso ✓ Link copied!

You need to share the code of the page. There's a link that's pointing to the wrong route. I can't point it out unless I see your code. Your screenshot only shows that you have a route that's not pointed correctly.

FC
Firman Canva ✓ Link copied!

out of title post, i have question about crash style panel filament, any solution ? https://prnt.sc/f4HvWJM4A5Ex

WV
Wouter van Dort ✓ Link copied!

I want to access data from the parent on the create page of the child. I couldn't find a way to get this parent id accessible in the childs resource. Does any of you have a solution for this.

M
Modestas ✓ Link copied!

It should be accessible via $this->parent->id if I'm not mistaken

WV
Wouter van Dort ✓ Link copied!

Yes, that is the case if I want to access it on the page. However, I am not able to pass it to the resource, so I put information from the parent into a placeholder

M
Modestas ✓ Link copied!

Do you have a demo of this somewhere? I did something similar and it worked without too much issues

WV
Wouter van Dort ✓ Link copied!

Within LessonResource:

class LessonResource extends Resource
{
use HasParentResource;
protected static ?string $model = Lesson::class;
protected static bool $shouldRegisterNavigation = false;
public static string $parentResource = CourseResource::class;
 
public static function getRecordTitle(?Model $record): string|null|Htmlable
{
return $record->title;
}
 
public static function form(Form $form): Form
{
return $form
->schema([
Forms\Components\Placeholder::make('Titel')->content(
function() {
return $this->parent->title;
}),
])
}

This throws the following error: Using $this when not in object context

M
Modestas ✓ Link copied!

You are right, there is a bit of a different workaround there. Add this to your Lesson resource:

class LessonResource extends Resource
{
public static string $parentResource = CourseResource::class;
 
protected static ?string $model = Lesson::class;
 
protected static bool $shouldRegisterNavigation = false;
 
public static function getParentInfo()
{
if ($parent = (request()->route('parent') ?? request()->input('parent'))) {
$parentResource = self::$parentResource;
 
return $parentResource::resolveRecordRouteBinding($parent);
}
}
 
public static function form(Form $form): Form
{
return $form
->schema([
Forms\Components\Placeholder::make('Titel')->content(
function () {
dd(self::getParentInfo());
return $this->parent->title;
}),

The static function getParentInfo() will load a model for you :)

WV
Wouter van Dort ✓ Link copied!

Thanks a lot for this solution. You have a small inconsistency in your code:

dd(self::getParentInfo());
return $this->parent->title;

should be:

return self::getParentInfo()->title;
WV
Wouter van Dort ✓ Link copied!

There is still something wrong when I also use a live() element in the resource. After a toggle, an error showed up on the return line: Attempt to read property "title" on null

M
Modestas ✓ Link copied!

Whoops! Sorry about the mistake, indeed I left it by accident (a bit of experimenting gone wrong haha).

Hmm, with the ->live() it could for sure cause some issues. Here, I don't have a direct answer as this whole thing is pretty much a hack. So at this point, can't help you much as it needs investigation and probably more code modifications, sorry!

WV
Wouter van Dort ✓ Link copied!

After some thinkering around, I found a solution which is also kind of a hack. The Post request generated by the ->live(), also contains a snapshot, where the parent variable can be retrieved:

public static function getParentInfo()
{
if ($parent = (request()->route('parent') ?? request()->input('parent'))) {
$parentResource = self::$parentResource;
return $parentResource::resolveRecordRouteBinding($parent);
}elseif(request()->isMethod('post')){
$req = request()->input("components");
$snapshot = json_decode($req[0]["snapshot"],true);
$parent = $snapshot["data"]["parent"][1]["key"];
$parentResource = self::$parentResource;
return $parentResource::resolveRecordRouteBinding($parent);
}
}
M
Modestas ✓ Link copied!

Awesome! This seems like a nice approach :)

M
MrGG ✓ Link copied!

Quick question... After setting up a Nested Resource as per your guide, my 'afterSave' actions on the on the Edit page stopped working. Same code works on non nested resources fine... Any suggestions welcome. Thanks.

M
Modestas ✓ Link copied!

Could you give an example of what stopped working? It should not have impacted any methods, but it may have conflicts with the code itself. So if you could share an example action and error message - that would help.

M
MrGG ✓ Link copied!

I had an afterSave action to simple show a notification of the 'id', running when a record was edited.

After I impliented it as a nested resource, it doesn't trigger.

protected function afterSave(): void
{
$record = $this->record;
Notification::make()
->title($record->id)
->success()
->send();
}
M
Modestas ✓ Link copied!

Hmm, I'm not sure why this would be the case. I would try to add dumps in multiple places to see why it doesn't get triggered.

We did not touch any of these methods so it's weird that they stopped working and calling hooks :/

M
MrGG ✓ Link copied!

Thank you for your replies.

I'll report back what I discover.

I
iotron ✓ Link copied!

The list page works as in the tutorial but when I use it with getRecordSubNavigation, it gives this error. Kindly help. 😢

public static function getRecordSubNavigation(Page $page): array
{
return $page->generateNavigationItems([
// error from navigationItem::url() on being included here
])
}

I tried the url() in the navigationItem for a page class. right now they are set to static and cannot be overridden in a ListRecord class. trying to override NavigationItem gives the error

public static function getNavigationItems(array $urlParameters = []): array
 
{
 
return [
 
NavigationItem::make(static::getNavigationLabel())
 
->group(static::getNavigationGroup())
 
->parentItem(static::getNavigationParentItem())
 
->icon(static::getNavigationIcon())
 
->activeIcon(static::getActiveNavigationIcon())
 
// ->isActiveWhen(fn (): bool => request()->routeIs(static::getRouteName()))
 
->sort(static::getNavigationSort())
 
->badge(static::getNavigationBadge(), color: static::getNavigationBadgeColor())
 
->url(fn (): string => EventsResource::getUrl('bookings.index', ['parent' => $this->parent])),
 
];
}
M
Modestas ✓ Link copied!

Sorry, I can't help you with sub navigation as I haven't used nested resources with it... This one you will have to debug and see why it happens

I
iotron ✓ Link copied!

I have debugged and found that the issue is coming from NavigationItem isActiveWhen() and Url() as they try to resolve the url from their own resource instead of the parent resource.

I am unable to find a solution though as getNavigationItems is static and we cannot override the url using $this->parent.

HR
Hani Ramadhan ✓ Link copied!

Can I make more than 2 nested resources with this approach?

M
Modestas ✓ Link copied!

this only supports 1 level of nesting. Going deeper is a lot harder

KS
Khant Si Thu Phyo ✓ Link copied!

dear all i use to create but it don't fixin with my require

here is what i want i have series, seasons,episodes

but record is only one get how do i make

Model

Series have many Season Season have belongto series and have many episodes episodes have belongto series

So route should be like that series/{record}/seasons/{record}/episodes

how can i make please guide me

M
Modestas ✓ Link copied!

Hi,

It is currently not possible to make more than one level of nested resources. So the max you can do is:

series/{record}/seasons

That's it, you can't do episodes on top.

GV
Gaurav V ✓ Link copied!

can we do it for multiple nested resource files? like courses has different modules and that modules have many lessons, like wise? Course will be main parent, module will be child of course and parent of lesson, lesson will be child of module. Please consider this also.

M
Modestas ✓ Link copied!

Hi, as mentioned in multiple previous comments - no, it is not possible to build more than 1 nested resource with this method

WG
Washington Guerrero ✓ Link copied!

Following this tutorial, I was able to add nested resources. How can I nest a custom Page to a parent resource?

The custom page is called settings, and It has a form with a bunch of fields.

I added the route to the parent's getPages(), like it was just another List/Create/Edit view:

...
//settings
'settings' => Settings::route('/{parent}/settings'),
...

The above will throw the following exception:

Method App\Filament\Customer\Pages\Settings::route does not exist.
MY
Merta Yoga ✓ Link copied!

Hi, is it possible to update the code so it support nested 3 level like: course -> lesson -> participant

M
Modestas ✓ Link copied!

Hi, this is not possible with the current setup (and Filament limitations). It should be possible with v4.

For now, I would recommend you to use Clusters (https://filamentphp.com/docs/3.x/panels/clusters)

HH
Hilmi Hidayat ✓ Link copied!

I tried to implement this tutorial on my use case. Where I have event management. Each event has speakers or sponsors. Here I implement nested resource for Speakers. When I try to Delete speaker record, I get error like below.

Filament\Resources\Pages\ListRecords::Filament\Resources\Pages{closure}(): Return value must be of type string, null returned

https://ibb.co.com/RkyL5Y51

However, when I delete the record via edit page, it works without any error.

Here are my codes

SpeakerResource.php

<?php
 
namespace App\Filament\Resources;
 
use Filament\Forms;
use Filament\Tables;
use App\Models\Speaker;
use Filament\Forms\Form;
use Filament\Tables\Table;
use Filament\Resources\Resource;
use Illuminate\Database\Eloquent\Model;
use Filament\Tables\Columns\ToggleColumn;
use Filament\Tables\Filters\SelectFilter;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Contracts\Support\Htmlable;
use App\Filament\Resources\SpeakerResource\Pages;
use Illuminate\Database\Eloquent\SoftDeletingScope;
use Filament\Tables\Columns\SpatieMediaLibraryImageColumn;
use Filament\Forms\Components\SpatieMediaLibraryFileUpload;
use App\Filament\Resources\SpeakerResource\RelationManagers;
 
class SpeakerResource extends Resource
{
public static string $parentResource = EventResource::class;
 
public static function getRecordTitle(?Model $record): string|null|Htmlable
{
return $record->title;
}
 
protected static ?string $model = Speaker::class;
 
protected static bool $shouldRegisterNavigation = false;
 
protected static ?string $navigationIcon = 'heroicon-o-rectangle-stack';
 
public static function form(Form $form): Form
{
return $form
->schema([
SpatieMediaLibraryFileUpload::make('photo')
->required()
->columnSpanFull()
->collection('speakers')
->responsiveImages()
->conversion('thumb')
->conversionsDisk('public')
->uploadingMessage('Uploading ...'),
Forms\Components\TextInput::make('name')
->required()
->maxLength(255),
Forms\Components\TextInput::make('occupation')
->required()
->maxLength(255),
Forms\Components\Toggle::make('is_active')
->required(),
]);
}
 
public static function table(Table $table): Table
{
return $table
->reorderable('sort_order')
->columns([
SpatieMediaLibraryImageColumn::make('photo')
->collection('speakers')
->conversion('thumb')
->width(150)
->height(150)
->label('Photo'),
Tables\Columns\TextColumn::make('name')
->searchable(),
Tables\Columns\TextColumn::make('occupation')
->

We'd Love Your Feedback

Tell us what you like or what we can improve

Feel free to share anything you like or dislike about this page or the platform in general.