Weave Code
Weaver
Helps Laravel developers discover, compare, and choose open-source packages. See popularity, security, maintainers, and scores at a glance to make better decisions.
Feedback
Share your thoughts, report bugs, or suggest improvements.
Subject
Message

Filament Nested Resources Laravel Package

guava/filament-nested-resources

Filament Nested Resources adds nested resource routing and management to Filament admin panels. Define parent-child resources, generate nested URLs, and keep breadcrumbs and authorization in sync so related records are created and edited in their parent context.

View on GitHub
Deep Wiki
Context7

Getting Started

Install and register in five minutes.

composer require guava/filament-nested-resources
php artisan vendor:publish --tag="filament-nested-resources-config"
php artisan vendor:publish --tag="filament-nested-resources-migrations" # only if the tag exists in your version

Register the plugin on the panel you actually want nested routes on — not globally:

// app/Providers/Filament/AdminPanelProvider.php
->plugin(\Guava\FilamentNestedResources\NestedResourcesPlugin::make())

Pin the constraint ("guava/filament-nested-resources": "^1.3") next to an explicit filament/filament version in composer.json. With effectively zero dependents upstream, an unpinned Filament bump is the most likely way this fails quietly.

Where to look first, in this order:

  1. vendor/guava/filament-nested-resources/src/Resources/NestedResource.php (or the trait equivalent) — read the actual method signatures before writing code. The public surface is small (getParentResource(), getParentResourceRelationship(), breadcrumb hooks), and the docs are thinner than the source.
  2. config/filament-nested-resources.php after publishing — check the route prefix, parameter naming, and whether there's an index-page toggle.
  3. php artisan route:list --path=projects — confirm the generated /{parent}/{child} shape and parameter names before you write any getUrl() calls.

First use case — pick a two-level pair with a real Eloquent relationship. Project hasMany Task is ideal: trivial schema, low blast radius, exerciseable end to end.

// app/Filament/Resources/TaskResource.php
use Guava\FilamentNestedResources\Resources\NestedResource;

class TaskResource extends Resource
{
    use NestedResource;

    protected static ?string $model = Task::class;

    protected static bool $shouldRegisterNavigation = false; // it lives under the parent now

    public static function getParentResource(): string
    {
        return ProjectResource::class;
    }

    public static function getParentResourceRelationship(): string
    {
        return 'tasks'; // must match the Eloquent relationship method name exactly
    }
}

Then on ProjectResource, add a row action linking into the child index so the nested page is discoverable rather than only reachable by typing a URL:

Tables\Actions\Action::make('tasks')
    ->label('Tasks')
    ->url(fn (Project $record) => TaskResource::getUrl('index', ['project' => $record]))

Smoke-test the full verb set — list, create, edit, delete — plus one deliberate cross-parent URL (/projects/2/tasks/7 where task 7 belongs to project 1) and confirm it 404s rather than leaking. That single test tells you whether the package's core promise holds in your app.

Implementation Patterns

Parent context is a route parameter — treat it as such everywhere. The plugin scopes resolution via the relationship, but the parent record still has to be threaded through every URL your app generates. Centralize this immediately:

// app/Filament/Resources/TaskResource.php
public static function urlFor(Task $task): string
{
    return static::getUrl('edit', [
        'project' => $task->project_id,
        'record' => $task->getKey(),
    ]);
}

Then audit and replace every raw TaskResource::getUrl(...) call site — widgets, notification actions, custom table actions, global search, email links. This is the estimate-killer on the first pair: budget the URL audit, not the install.

Scope the nested table explicitly rather than trusting the implicit scope. Even if the plugin filters for you, make the intent legible and testable:

// TaskResource\Pages\ListTasks
protected function getTableQuery(): Builder
{
    return parent::getTableQuery()
        ->whereBelongsTo($this->getParentRecord()); // confirm accessor name in the trait
}

The benefit is the query-count assertion: DB::enableQueryLog() around a nested index request catches the N+1 that hierarchical listings invite — parent record, child list, and each child's own relations.

Associate on create, don't rely on a hidden field. In CreateTask, either set the foreign key in a mutateFormDataBeforeCreate() hook from the route-bound parent, or disable a parent select entirely. Leave a select visible and someone will create an orphan from the nested URL, which then 404s on the next visit.

Breadcrumbs are parent-derived — verify them at three levels before you trust them. 1.3.0 was a breadcrumb-only release, so this is the freshest code path. Org → Project → Task should render two linked ancestors and the current record as plain text. If you subclass a resource page with a custom getBreadcrumb() or getBreadcrumbs(), that override bypasses the new defaults — those overrides are exactly what 1.3.0 changed behaviour underneath.

Redirects after save need the parent too. Filament's default getRedirectUrl() on a Create/Edit page often points at the flat resource index. Override it on nested pages to return the nested index, otherwise every successful save bounces the user out of the hierarchy they were working in.

Keep the flat child routes live during transition. Nothing in the package requires you to delete them. Leave them registered (temporarily re-enabling navigation if you must) so bookmarks, existing feature tests, and outbound notification links keep resolving. Retire them after pilot sign-off, not during.

Decide per pair: RelationManager or nested resource. The rule to write into your conventions doc — use the plugin only where children need first-class pages: their own table with filters, their own widgets, custom pages, or shareable deep links. If children only need inline CRUD under the parent, a RelationManager is less route surface, no URL audit, and one fewer dependency.

Multi-tenancy: scope the parent yourself. In a tenant-aware panel, verify that the plugin resolves the parent through the tenant-scoped query builder. Do not assume it does — assert it with a two-tenant negative test (tenant A's user hitting tenant B's /projects/{id}/tasks). If it doesn't scope, wrap resolution in your own modifyQueryUsing / resolveRecordUsing.

Gotchas and Tips

Relationship scoping is not authorization. The single biggest trap. The plugin prevents /projects/2/tasks/7, but that is record resolution, not a policy check. You still need TaskPolicy covering view/update/delete, and — critically — you need to verify whether your child policies receive the parent record at all. If they don't, parent-scoped rules ("users can only edit tasks in projects they own") require bespoke checks in the resource or a scopeQuery in the policy. Test all four verbs plus a second-tenant negative; scoping passing does not imply authorization passing.

getParentResourceRelationship() must return the literal Eloquent method name. 'tasks', not 'Task', not the model class. A mismatch surfaces as an empty table or a 500 during binding, and the error message usually points at the query, not the declaration. If a nested index is mysteriously blank, check this first.

Route cache is a real failure mode. Run the full nested route suite after php artisan route:clear and again after php artisan route:cache. Nested routes are dynamically registered; anything that doesn't survive the cache round-trip will pass locally and 404 in production. Make it a CI step.

Global search will break silently. Filament's global search builds URLs from resource conventions and typically has no parent parameter to work with. Either exclude nested child resources from global search (protected static bool $isGloballySearchable = false) or override getGlobalSearchResultUrl() to include the parent. Otherwise every search hit into a child is a 404 that nobody notices until a user reports it.

Hide the child from navigation, but consider the orphan problem. $shouldRegisterNavigation = false is correct, but it means the only entry point is a parent-side action. Add that action on every parent list and view page, or the feature is invisible to the people it's for.

Version pinning and the upgrade tax. Filament majors are the primary breaking vector — nested route registration sits close to Filament's resource and route contracts. Treat a Filament upgrade as blocked until the nested suite passes. With near-zero downstream adoption you will discover regressions before anyone reports them, so plan on reading the package's composer.json yourself rather than waiting for a compatibility release.

Extension points, ranked by value for effort:

  • Override getBreadcrumbs() / getBreadcrumb() on nested pages — cheap, immediate UX win, but the code path 1.3.0 changed, so regression-test custom overrides specifically.
  • Custom page classes per nested action when you need parent-aware headers, tabs, or summary widgets.
  • Route prefix or per-pair gating rather than a panel-wide switch — supports rolling out pair by pair.

Testing checklist to codify once and reuse per pair:

it('scopes nested records to the parent', function () { /* cross-parent 404 */ });
it('enforces policy on all four verbs', function () { /* incl. a second-tenant negative */ });
it('renders ancestor breadcrumbs as links', function () { /* three levels */ });
it('does not N+1 the nested index', function () { /* assert query count */ });
it('generates nested urls from the child resource', function () { /* urlFor() */ });

Name an owner. A package with no SLA and no downstream ecosystem becomes orphaned infrastructure the moment nobody is accountable for the version pin, the getUrl() convention, and that test file. Without it, this quietly becomes the thing that blocks your next Filament upgrade.

Weaver

How can I help you explore Laravel packages today?

Conversation history is not saved when not logged in.
Prompt
Add packages to context
No packages found.
elsayed85/laravel-api-exceptions
ouhssini/laravel-demo-mode
daworks/ncloud-outbound-mailer-driver
gp247/cms
ignibyte/aic-gates
nexa-merchant/blog
novasa/laravel-language-center
opencage/geocode
adolfoholzer/parameters
livenetworks/ln-starter
shitoudev/permissionmanager
johannesclimacus/laravel-tui
hocvt/seotools
vitalybaev/sypexgeo
amphibee/themosis-ignition
gito-me/fast-excel
frankyso/laravel-ipaymu
geolize-studio/laravel-chile
boomerangui/chat
cgv/localized-resource