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

Nestablecollection Laravel Package

hsharghi/nestablecollection

NestableCollection is a Laravel package that extends collections for working with nested, hierarchical data. Build trees, traverse parents and children, and manage nested sets with a familiar collection API, keeping your models and arrays organized.

View on GitHub
Deep Wiki
Context7

Getting Started

The package adds hierarchical (parent/child) behaviour to Eloquent models. The 1.1.20 release adds a fluent parentColumn() method so you can point the package at a non-default foreign key instead of the assumed parent_id.

  1. Install and (if the package ships one) publish its config:
    composer require <vendor>/<package>
    php artisan vendor:publish --tag=<package>-config
    
  2. Ensure the table has a nullable, indexed self-referencing column. The default is parent_id; if your schema already uses something else (pid, parent_uuid, mother_id), keep it and override it in code:
    Schema::create('categories', function (Blueprint $table) {
        $table->id();
        $table->foreignId('parent_id')->nullable()->index();
        $table->string('name');
        $table->timestamps();
    });
    
  3. Add the package trait to the model and declare the parent column before any query runs:
    class Category extends Model
    {
        use HasTree; // trait name per package docs
    
        protected static function booted(): void
        {
            static::parentColumn('parent_id'); // new in 1.1.20
        }
    }
    
  4. First use case to validate the setup: render a simple two-level menu.
    $roots = Category::query()->whereNull('parent_id')->with('children')->get();
    
    If children comes back empty while rows clearly have parents, the column override is usually the culprit — see Gotchas.

Look first at the package's trait (for available scopes/relations) and its config file (for column names, depth limits, and caching). Those two files answer most day-to-day questions.

Implementation Patterns

Declare the parent column in the model, not the caller. parentColumn() is model-level configuration. Setting it in a controller or a random query builder chain is fragile — it will not apply to relations, scopes, or code paths you did not touch. Centralise it in booted() or a protected static $parentColumn property if the package supports both.

Legacy schema bridging. The main reason to reach for parentColumn() is adopting the package on an existing table. Map the package's expectations onto your reality:

static::parentColumn('pid');        // legacy column
static::orderColumn('sort_order');  // if the package exposes this too

Keep the mapping in a single base model or trait so every child model (categories, comments, pages) inherits it.

Tree-building for API responses. Fetch once, assemble in PHP, rather than recursing per node:

$flat = Category::query()->orderBy('name')->get();
$tree = $flat->toTree();            // or your own recursive collection helper
return CategoryResource::collection($tree);

This avoids N+1 queries and keeps response latency flat as depth grows.

Eager loading to a known depth. For breadcrumbs and menus you rarely need the whole tree:

Category::with(['parent.parent.parent', 'children.children'])->find($id);

Or, if the package provides descendants/ancestors relations, constrain them rather than materialising everything.

Moving and re-parenting. Wrap moves in a transaction and re-read the model afterwards; most implementations mutate sibling ordering or path columns as a side effect:

DB::transaction(function () use ($node, $newParent) {
    $node->parent()->associate($newParent)->save();
});
$node->refresh();

Multi-tenancy. If trees are scoped per tenant, the parent column alone is not enough — constrain every query by tenant and validate on move that the new parent belongs to the same tenant:

abort_unless($newParent->tenant_id === $node->tenant_id, 422);

Without this, users can graft a node under another tenant's parent and the tree queries will happily follow it.

Consistent ordering. Trees are unordered by default. Pick an explicit order column or a deterministic orderBy for siblings, or your menu ordering will drift between requests.

Gotchas and Tips

  • parentColumn() must be set before the first query. Static configuration called lazily (e.g. inside a scope) will not affect relations already resolved or eager loads already planned in the same request.
  • Relation names do not follow the column automatically in every package. After switching to pid, verify parent(), children(), ancestors(), and any withCount calls; some packages derive relation keys from the configured column, others hard-code parent_id.
  • Mass assignment and the parent column. If you override the column, remember to add it to $fillable and to your validation rules — otherwise associate() works but create(['pid' => ...]) silently drops it.
  • Self-referencing parents create cycles. Validate on save that the new parent is not a descendant of the node. Otherwise a bad PUT produces an infinite loop in any recursive traversal, and the failure surfaces as a timeout, not an exception.
  • Depth is a business constraint, not a package one. Guard against runaway nesting at the application layer; unlimited depth times eager loading is a reliable way to blow memory in a menu endpoint.
  • Caching. Tree queries are read-heavy and easy to cache, but any write through save(), delete(), or re-parenting must invalidate the cache key. Key per tenant plus per root node if you cache subtrees.
  • Debugging tips. Log the resolved column ($model->getParentColumnName() or equivalent) in a failing test, and toSql() a suspicious scope. Nine times out of ten, a "tree is empty" bug is a column mismatch or a missing index, not a logic error.
  • Index the parent column. Every child lookup does where parent_column = ?; without an index this degrades linearly on large tables.
  • Testing. Assert on both the column name and the traversal result in one test: create a three-level fixture using your non-default column, then assert children, ancestors, and re-parenting behave identically to the parent_id default. This catches regressions when the package changes its column resolution again.
  • Extension points. If you need behaviour the trait does not expose, prefer composing a custom trait that uses the package trait and overrides the accessor for the parent column, rather than patching vendor code — the next release will overwrite it.
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.
payerurl/telegrambot-crypto-payment-instant-payout
dskripchenko/laravel-hybrid-search
riorizkyrainey/lara-ced
mehedimi/static-vel
zharp/zharp
nonsapiens/bigquery-model-sync
lgu-bayambang-ict-office/lgu-laravel-development-standards
andydefer/laravel-support
se7enxweb/flysystem
lampwick/laravel
jeslage/statamic-emojis
micahdshackelford/rmap-laravel
draftsmaninc/draftsman
square-bit/laravel-volition
himito/laravel-mailman
htinlynn/genius-helper
maco-studios/hephaestus
elmogy/larafast
xeviant/laravel-paystack
gorilladash/laravel-website-sdk