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

Nova Nested Tree Attach Many Laravel Package

phoenix-lib/nova-nested-tree-attach-many

Phoenix Lib's Nova Nested Tree Attach Many adds a Laravel Nova field for selecting and attaching many related records through a nested tree UI. Browse hierarchical data, search nodes, and manage parent-child selections directly from Nova forms.

View on GitHub
Deep Wiki
Context7

Getting Started

Confirm compatibility before you write a line of PHP. This is a Nova 3 / Vue 2 era field — last release 1.8.0 shipped September 2021, predating Nova 4/5. Open the package's composer.json and package.json and check the Nova, Laravel, and PHP constraints against your install. If you're on Nova 4+, a Vue 2 component won't mount, and you're committing to a fork-and-patch before you've seen the UI. Ten minutes here saves a wasted spike.

Install and pin exactly:

composer require phoenix-lib/nova-nested-tree-attach-many:^1.8
php artisan vendor:publish --provider="PhoenixLib\NovaNestedTreeAttachMany\FieldServiceProvider"

Never float on dev-master. With ~35 stars, zero dependents, and one maintainer, the realistic operating model from day one is fork at 1.8.0 and pull via a VCS repository. The source is tiny and MIT-licensed, so this costs minutes and buys you a patch lane when Nova upgrades or a bug appears.

Prerequisites you must already have:

  • Laravel Nova installed and rendering other fields fine.
  • A BelongsToMany relationship on the model — including self-referential (e.g. Category belongsToMany Category through a category_category pivot) with multiple parents allowed. If your schema is a single parent_id, the attach-many shape is wrong for you.
  • The related Nova Resource registered — the field renders those records through it.

Where to look first, in order:

  1. README.md — field options and the relation-less (Flexible Content) example live here, not in code comments.
  2. src/NestedTreeAttachMany.php — the authoritative list of chainable methods. Ignore blog snippets; use only what this class exposes.
  3. resources/js/components/ and dist/js/ — when the field renders blank, the problem is almost always a stale dist, not your config.
  4. composer.json / package.json — the real compatibility statement.

First use case — the standard relationship field:

use PhoenixLib\NovaNestedTreeAttachMany\NestedTreeAttachMany;

public function fields(Request $request)
{
    return [
        ID::make()->sortable(),
        Text::make('Name'),
        NestedTreeAttachMany::make('Categories', 'categories', Category::class),
    ];
}

Verify the exact argument signature against the source first — some Nova fields take (name, attribute, resourceClass), others infer the resource from the relationship name. Don't guess.

Your acceptance test is a loop, not a feature checklist. Seed 10 nodes, then 500. Open the form → expand the tree → attach two children → save → reopen and confirm they're checked → detach one → save. Then re-save with no changes and confirm nothing moved. If that passes, you're integrated — this is a half-day job, not a project.

Compare against the zero-dependency option first. Stand up a branch using core Nova's BelongsToMany field with search. The tree is a UX nicety; the dependency is forever. If the tree isn't a genuine editorial requirement, take the simpler option.

Implementation Patterns

Pattern 1 — canonical attach-many on a self-referential model. Product belongsToMany Categories; Category belongsToMany Category for hierarchy. Editors expand branches and tick nodes; persistence flows through Nova's ordinary pivot handling. Downstream this is just Eloquent — $product->categories, eager loading, pivot columns, policies all behave normally. No service layer, and removal is a one-line field deletion. This is the mode where the package earns its keep.

Pattern 2 — scoping and authorization live behind the field, never in it. The package bakes in no permissions, tenancy, or audit trail. Enforce those where they already belong:

  • authorizedToViewAny / authorizedToAttachAny on the related resource.
  • A global scope on the tree model for tenant/active filtering.
  • Nova activity logging on the parent record.

A field that shows nodes the policy forbids attaching is a real security bug, and the package won't catch it. Test it explicitly — log in as a restricted user and assert the forbidden branches don't render.

// Related resource
public static function authorizedToAttachAny(Request $request): bool
{
    return $request->user()->can('attachCategory', Category::class);
}

Pattern 3 — pivot payloads, only if the field supports them. If you need position, label, or ordering on the pivot, confirm the field passes pivot values through rather than doing a bare ID attach. If it doesn't, don't fight the component: attach through the field, then enrich the pivot via a Nova Action, a model observer, or a purpose-built admin screen. This field is a selector, not a pivot editor.

Pattern 4 — relation-less mode inside whitecube/nova-flexible-content (new in 1.8.0). This lets a Flexible Content block carry a hierarchy selection with no backing Eloquent relation per block type — useful when marketing adds layouts without schema churn:

// Inside a Flexible Content layout's fields()
NestedTreeAttachMany::make('Categories'), // no relationship behind it

The trade is that you own persistence and integrity. Decide and document the stored shape once — flat array of IDs or full node payloads. Flat IDs are smaller and rehydrate cleanly; node payloads survive deletion but silently go stale. Your block serializer must then handle:

  • Orphan pruning. Nothing removes deleted category IDs from a JSON blob. Add a read-time filter (Category::whereIn('id', $ids)->pluck('id')) or a scheduled cleanup, or pages render broken references months later.
  • Validation. JSON columns bypass foreign keys — add a custom Rule that every ID exists and is attachable.
  • Render cost. Each block loads the whole tree; several blocks per form multiply query count.

Treat this path as beta: it's the newest code, documented by example rather than by a serialization spec.

Pattern 5 — fork-and-pin as the default operating model. "Code we own" is the honest stance at this scale. Fork at 1.8.0, add a smoke test, pull via VCS:

"repositories": [
  { "type": "vcs", "url": "[email protected]:ourorg/nova-nested-tree-attach-many.git" }
]

Minimum smoke-test coverage: attach two nodes → assert pivot rows; detach one → assert the exact resulting set; in relation-less mode, a JSON round-trip plus an orphan-pruning assertion. That one test is what prevents a Nova upgrade from silently breaking an ops workflow.

Day-to-day workflow. Most edits happen in exactly three places: the resource's fields() (adding options), a global scope on the related resource (which nodes appear), and model observers on both sides of the pivot (cache flush, search index, denormalized counters). Reach for the fork only when you need behavior beyond that surface.

1.8.0 has one non-inert behavior change. It adds a BelongsTo cache flush after save for models using laravel-eloquent-query-cache. That's helpful, but it means the upgrade does change invalidation behavior for touched models — review the diff and confirm the flush is scoped rather than global before rolling out.

Gotchas and Tips

Blank field. The classic Nova package failure. Symptom: empty box or nothing at all, no PHP error. Fix in order: php artisan vendor:publish --force, clear Nova's asset cache, hard-refresh with cache disabled, then check the browser console — a missing Vue component surfaces as an "unknown custom element" warning.

Nova version lock-in is the deciding risk, not code quality. On Nova 4+ (Vue 3) expect the field not to load at all, or expect to patch the Vue component in your fork. Budget the fork as the actual cost of adoption. If that patch looks like more than a week, writing your own tree-select against your Nova version is a legitimate alternative.

Detach semantics can silently delete data. Attach-many fields differ on whether save performs a full sync or is additive. Prove it on a fixture before production:

  1. Attach A and B, save.
  2. Re-open, change nothing, save.
  3. Re-read the pivot rows.

If step 3 wipes rows, you have sync semantics, and ops will experience it as lost categorizations. This is the most common quiet data-loss mode in attach-many fields — write an explicit regression test, keep pivot backups until semantics are proven, and route bulk re-categorization through a Nova Action you control.

Whole-tree loading and the node ceiling. Every form load pays to render the tree. Test at 500 and 5,000 nodes and watch the query log plus browser render time. There is no lazy loading or server-side search in a 2021-era Vue 2 tree — at scale the tree becomes an admin UX liability, and relation-less mode with several blocks per form multiplies it. If you hit the ceiling: scope the tree query to a subtree, or drop back to a searchable BelongsToMany.

Index your tree columns. Field performance is schema performance. Ensure parent_id on the related table and both pivot columns are indexed. A self-referential tree without a parent_id index looks exactly like a package problem when it's a query-plan problem.

Tree shape assumptions. "Nested tree" doesn't guarantee the storage strategy — adjacency list, kalnoy/nestedset, or something custom. Check whether the tree source is configurable or hardcoded. If your model already uses nested sets, confirm the field reads those columns rather than assuming adjacency; a mismatch shows up as a flat or half-empty tree.

Relation-less mode shifts the burden downstream. Values live in a parent JSON column, not a pivot. You lose FK integrity, cascade behavior, and DB-level validation. Any reporting or search that joins on categories will silently miss block-embedded selections — if downstream features consume those categories, that's the argument for relational mode instead.

Debug quickly with the network tab on the Nova form:

  • Tree payload returned but UI blank → frontend/asset problem (re-publish, inspect dist).
  • Request 404s or errors → route/service-provider or relationship-definition problem.
  • Confirm the relationship name passed to the field matches the model method exactly — Nova renders nothing for a mistyped relation.
  • Still stuck? dd() inside the field's resolve method to see what's hydrated.

Extension points worth knowing. The field is a thin wrapper. Real customization happens in three places: chainable methods on the field instance (read the source for the current list), global scopes on the related resource limiting visible nodes, and model observers on both sides of the pivot to keep caches, indexes, and counters in sync. Beyond that, fork — the surface is small enough that patching your fork is cheaper than lobbying upstream.

Upgrade discipline. Pin exact versions, pin your fork, and re-run the smoke test on every Nova upgrade. composer update against a package with a bus factor of roughly one is how an internal admin screen breaks on a Friday. The 1.8.0 release shows maintenance is happening, but treat it as "lightly maintained," not "supported" — carry the test, carry the fork, and keep your exit cost at a single field declaration.

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.
socialdept/atp-orm
cboxdk/laravel-assistant-client
socialdept/atp-resolver
socialdept/tenant-domains
tautvydasr/blade-class-names
socialdept/color-scales
maherelgamil/controller
coleus/widgets
coleus/table
coleus/users
coleus/music
coleus/health
coleus/calendar
coleus/notes
coleus/support
coleus/settings
coleus/apps
seampact/laravel
emmyconceptng/laravel-ng-postcode
jjsquady/round-robin