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

Nested Tree Filter Laravel Package

gekich/nested-tree-filter

A Laravel package for filtering nested tree structures, making it easier to query hierarchical data like categories and menus while preserving parent-child relationships in filtered results.

View on GitHub
Deep Wiki
Context7

Getting Started

The package is a Laravel Nova filter that renders a hierarchical (parent/child) tree in the filter panel, so index views can be narrowed to a node and — depending on config — its descendants. With 6 stars, no dependents, and a 2021 archive date, treat it as a reference implementation you fork, not infrastructure you depend on.

Minimal steps:

  1. Confirm the Nova version you run (3.x vs 4.x). The package predates Nova 4's build tooling and is almost certainly constrained to laravel/nova: ^3.0 in its composer.json. Check that file before anything else.
  2. composer require gekich/nested-tree-filter in a scratch branch, or clone the repo into packages/ with a path repository if you need to patch it.
  3. Publish/compile assets if the package ships a Vue component (php artisan vendor:publish, then npm run dev inside the package directory — Nova 3 packages typically require this, and Nova 4 uses nova.mix).
  4. Register the filter on the resource whose model participates in the tree:
// app/Nova/Product.php
public function filters(Request $request)
{
    return [
        \Gekich\NestedTreeFilter\NestedTreeFilter::make('Category')
            ->model(\App\Models\Category::class),
    ];
}
  1. Make sure the underlying model actually exposes the shape the filter expects: a self-referencing parent_id, a children() / parent() relation, and a display column.

Where to look first: the filter class's apply() method and the options/constructor — that's where the author's assumptions about column names, relation names, and depth live. The README is thin at this star count; the source is the documentation.

First use case: a Category tree (3–4 levels) driving a Product index. Selecting "Electronics" should return products in Electronics and Phones, Laptops, etc. That single flow exercises every part of the package: tree rendering, descendant resolution, and the apply() query.

Implementation Patterns

Filter a belongsTo relation by subtree. The filter's value is a node ID; apply() must expand it to the node plus all descendants before constraining the query. If the package doesn't do this for you, override it:

public function apply(Request $request, $query, $value)
{
    $ids = $this->descendantIds((int) $value); // includes $value itself

    return $query->whereIn('category_id', $ids);
}

protected function descendantIds(int $root): array
{
    $ids = [$root];
    $frontier = [$root];

    while ($frontier) {
        $children = Category::whereIn('parent_id', $frontier)->pluck('id')->all();
        $frontier = array_diff($children, $ids);
        $ids = array_merge($ids, $frontier);
    }

    return $ids;
}

Prefer a bounded-depth recursion over unbounded. A while loop against a corrupted tree (a node whose ancestor is itself) runs forever. Add a depth cap or a visited set.

For large trees, swap the recursion for a recursive CTE (MySQL 8 / Postgres) or a materialized path / nested-set column. A 20k-node category tree resolved with one query per level is the usual production failure mode here.

Register the same filter with different scopes when a resource has multiple tree-shaped dimensions:

NestedTreeFilter::make('Category')->model(Category::class),
NestedTreeFilter::make('Region')->model(Region::class)->key('region_tree'),

Give each a distinct key() — Nova derives the query-string key from the filter's name, and two filters named after their class will collide.

Set a sensible default for dashboards where an unfiltered view is meaningless:

NestedTreeFilter::make('Category')
    ->model(Category::class)
    ->default($request->user()->default_category_id),

Combine with Nova's built-in filters freely — the tree filter is just another where/whereIn clause, so it composes with RangeFilter on dates or a BooleanFilter for status without ordering concerns.

Customize labels without touching the model by passing an accessor-produced attribute as the label column (e.g. an indented_name accessor that prepends — per depth). The tree UI rarely renders indentation itself; you supply it.

Eager-load the tree once. If the filter's options are computed on every request (common in these packages), cache the flattened list per model with a short TTL and clear it on save via an observer.

Gotchas and Tips

  • Archived and unmaintained. No commits since 2021-11-18. Nova's filter API, asset pipeline, and Vue version have all moved. Budget for a fork, a composer.json constraint bump, and recompiling the JS. If you only need the tree widget, a 120-line custom Nova filter built on SelectFilter plus a descendant query is often cheaper than resurrecting this.
  • Nova 4/5 compatibility is unverified. Nova 4 replaced the webpack.mix.js package build with laravel-nova's own tooling; a Nova 3 component will silently fail to register and you'll get a blank filter panel entry rather than an error.
  • apply() may not recurse. Verify against source before trusting the README. If it only filters where('parent_id', $value) or where('id', $value), it filters one level, and your users will report "the filter is broken" when a parent category shows nothing.
  • Missing index on parent_id. Add it. Recursive descent without it degrades to a table scan per level.
  • Cycles and orphans. parent_id pointing at a deleted row, or a two-node cycle, will either infinite-loop the resolver or drop nodes from the tree silently. Guard both in apply() and add a Category::saving check that rejects self-parenting.
  • Query-string persistence. Nova encodes filter state in the URL; a stale ?category_tree= value for a deleted node yields an empty index that looks like a bug. Validate the incoming ID and ignore unknown ones.
  • Octane/queue safety. Any static cache of the flattened tree inside the filter class survives between requests under Octane. Key it by model class and invalidate explicitly, or skip caching.
  • Asset publishing is a separate step from composer install. After upgrading the package inside your app, re-run the publish command and npm run build, or you'll debug a stale Vue component for an hour.
  • Testing: assert on apply() directly rather than through Nova's HTTP layer — instantiate the filter, build a Builder, call apply with a Request carrying the filter key, and assert the SQL bindings contain the full descendant set. That's the whole contract worth testing.

Extension points, in order of usefulness: subclass the filter and override apply() (keeps the tree UI, replaces the query logic); override the options/options() method to control what's listed (e.g. hide archived branches); replace the Vue component with your own if you need search-inside-tree or multi-select, which this package almost certainly lacks.

Bottom line for day-to-day use: the UI idea is right and the implementation is small enough to audit in an afternoon. Adopt the pattern, not the dependency — pin it in a private fork you control, or reimplement the ~100 lines against the Nova version you actually run.

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.
pharaonic/laravel-notification-dependencies
beautinow/translug
dev-brainiac/bot-cloaked
octoberrain/html
octoberrain/extension
octoberrain/exception
octoberrain/contracts
frankyso/ipaymu
3mdikram/worldmoneywords
snowsoft/config
itmosoft/laravel-subscriptions
larva/laravel-ai-qwen
adachsoft/api-version-bundle
cgv/localized-resource
noith/settings
kda/laravel-package
kda/filament-dynamic-navigation
bypersoft/signbox
pairus/product-data
fayne/laravel-filament-cms-core