Skip to main content

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

Read more here
Premium Members Only
Join to unlock this tutorial and all of our courses.
Tutorial Premium Tutorial

Filament Nested Resources: Manage Courses and their Lessons

April 13, 2023
8 min read

If you have two Resource Controllers like Courses and Lessons, they are often called nested resources in Laravel. In this tutorial, I will show you how to make nested resources in Filament.

For this tutorial we will have two models Course, and Lesson. The course will have many Lessons, and Lessons will belong to a Course.

And this is what we will be building:

  1. In the list of courses, you will see a link to manage lessons of that course
  2. The page for managing lessons will show the title of the course and breadcrumbs including that course title

courses page

lessons page


Prepare Resources

First, we will prepare resources. In the LessonResource, we need to set a new route and change create a route so that it would have a record, change a slug URL, set that it won't be registered in the navigation, and change the query so that it would get lessons only for the selected course.

app/Filament/Resources/LessonResource.php:

class LessonResource extends Resource
{
protected static ?string $model = Lesson::class;
 
protected static ?string $slug = 'courses/lessons';
 
protected static bool $shouldRegisterNavigation = false;
 
public static function form(Form $form): Form
{
return $form
->schema([
Forms\Components\TextInput::make('title')
->required(),
]);
}
 
public static function table(Table $table): Table
{
return $table
->columns([
Tables\Columns\TextColumn::make('title')
->searchable()
->sortable(),
])
->filters([
//
])
->actions([
Tables\Actions\EditAction::make(),
])
->bulkActions([
Tables\Actions\DeleteBulkAction::make(),
]);
}
 
public static function getPages(): array
{
return [
'index' => Pages\ListLessons::route('/'),
'lessons' => Pages\ListLessons::route('/{record}'),
'create' => Pages\CreateLesson::route('/{record}/create'),
'edit' => Pages\EditLesson::route('/{record}/edit'),
];
}
 
public static function getEloquentQuery(): Builder
{
return parent::getEloquentQuery()->where('course_id', request('record'));
}
}

Now, that we have created a route for listing lessons, we can add an action to the CourseResource to list lessons.

app/Filament/Resources/CourseResource.php:

class CourseResource extends Resource
{
protected static ?string $model = Course::class;
 
protected static ?string $navigationIcon = 'heroicon-o-collection';
 
public static function form(Form $form): Form
{
return $form
->schema([
Forms\Components\TextInput::make('title')
->required(),
]);
}
 
public static function table(Table $table): Table
{
return $table
->columns([
Tables\Columns\TextColumn::make('title')
->searchable()
->sortable(),
Tables\Columns\TextColumn::make('lessons_count')
->counts('lessons'),
])
->filters([
//
])
->actions([
Tables\Actions\EditAction::make(),
])
->bulkActions([
Tables\Actions\DeleteBulkAction::make(),
])
->prependActions([
Tables\Actions\Action::make('View lessons')
->color('success')
->icon('heroicon-s-view-list')
->url(fn (Course $record): string => LessonResource::getUrl('lessons', ['record' => $record]))
]);
}
 
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'),
];
}
}

After creating a couple of courses you should see result like the bellow:

course list page


Creating Lesson

Before creating a lesson, we need to modify the URL of the create action.

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

class ListLessons extends ListRecords
{
protected static string $resource = LessonResource::class;
 
protected function getActions(): array
{
return [
Actions\CreateAction::make()
->url(fn (): string => LessonResource::getUrl('create', ['record' => request('record')])),
];
}
}

There are a couple of ways to set the course_id field. For this tutorial, we will use...

Premium Members Only

This advanced tutorial is available exclusively to Laravel Daily Premium members.

Premium membership includes:

Access to all premium tutorials
Video and Text Courses
Private Discord Channel

Comments & Discussion

KJ
Kevin Jiang ✓ Link copied!

Really helpful! Thank you Povilas!

A
adithyaricky ✓ Link copied!

I really wish I could save this tutorial, like dev.to

PK
Povilas Korop ✓ Link copied!

What do you mean by "Save"? You mean "Bookmark"? Then use your browser function for it, should work.

MS
Mike Scott ✓ Link copied!

Hey Povilas,

I think he means favourite courses and tutorials lists which can be added to by "hearting" a course or tutorial. That way we could have a favourites page which lists them under our user, regardless of what device we're logged in on.

Adding a favourite or bookmark to the browser is limited to the device you're on unless you have shared these. And it makes it more difficult to find in 6 months time when you're looking for it.

PK
Povilas Korop ✓ Link copied!

Yeah, I see your point, James. But there are external tools for bookmarks like Pocket, and browsers can be synced between devices.

But I'll think about where we can fit this function into our schedule, for now we're focused on creating content and, well, a bit of Summer vacations :)

L
lloricode ✓ Link copied!

Thanks for this <3

C
Celev ✓ Link copied!

When trying a sort a Column on the Lessons Table, it throws the following error: How could i get it back to be able to sort columns again ?

Missing required parameter for [Route: filament.resources.courses/lessons.lessons] [URI: admin/courses/lessons/{record}] [Missing parameter: record].

N
Nerijus ✓ Link copied!

To solve your error after you press sort you don't have record from the request. To get it simply you can you do it like so

request('record') ?? explode('/', request()->fingerprint['path'])[3]

But unfortunately it does not solve sorting problem 100%. It then just gives no records but after page refresh it works. And sorry, don't know how to fix it. Also, while writing this tutorial package filament-nested-resources was released. It's a wip package but you could try using it. Or try official filament discord to get help, maybe someone from the core team will help sove this.

M
mrglazzz ✓ Link copied!

thx, this thing worked for me: request('record') ?? explode('/', request()->fingerprint['path'])[3]

MS
Mike Scott ✓ Link copied!

If you would like the link for the course in the breadcrumbs to go to the course's edit page, you can simply change the second breadcrumb in the array from:

'#' => $this->course->title,

to:

CourseResource::getUrl('edit', ['record' => $this->course]) => $this->course->title,
M
Muzafar ✓ Link copied!

very helpful, Thank you Povilas!

One issue right now i am facing is while deleting any child detail, it was throwing the error: Missing required parameter for [Route: filament.resources.lookups/lookupDetails.lookupDetails] [URI: admin/lookups/lookupDetails/{record}] [Missing parameter: record].

but then I saw the comment of Nerijus, and implemented the same, now instead of throwing error, the pop up for delete appears and it also give a success message but the record is not deleting. Further to this, the sorting is not working on child page and it shows empty page.

request('record') ?? explode('/', request()->fingerprint['path'])[3]

Q
Quink ✓ Link copied!

Great article. I'm getting following error when clicking on the "Create new lesson" button though.

Target Illuminate\Database\Eloquent\Model] is not instantiable.

Edit and Delete are just working fine. No idea whats causing this error message. Filament version I'm using is v3 though, not sure if it's related to this. Any ideas anyone?

GA
Ghiath Al-Khatib ✓ Link copied!

+1

PK
Povilas Korop ✓ Link copied!

In Filament v3, this tutorial doesn't work, unfortunately. In general, nested resources are not supported in Filament, you can do it in a "hacky" way but probably shouldn't.

MV
Mark van Barneveld ✓ Link copied!

Running in to the same issue.. What do you guys suggest as the most robust and stable solution? Can't get my head around it..

Q
Quink ✓ Link copied!

What I've actually done is creating a custom and edit page and registering it within the childs resource within the method getPages().

Also added mount and getEloquentQuery method into childs resource to get the correct childs and make the filter of the table functioning within the requested parent. If not it would just display all children from all parents.

public function mount()
    {
        $this->currentUrl = url()->current();
    }
		
public static function getEloquentQuery(): Builder
    {
        $path = explode('/', url()->current());

        if ($path[4] == 'trend-detections' && isset($path[5])) {
            Cache::set('filter_location_id', $path[5]);
        }

        return parent::getEloquentQuery()->where('location_id', Cache::get('filter_location_id'));
    }

For the custom pages, they have a getFormSchema, mount and submit method which will handle the required actions for me.

I might not be a good method but it worked for us :)

PK
Povilas Korop ✓ Link copied!

We've found a solution, and we are working on the update to this tutorial for Filament 3, should be released in 1-2 weeks, currently in "testing" phase :)

MV
Mark van Barneveld ✓ Link copied!

Thanks for the heads-up guys!

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.