lex/tree
Lex Tree is a Laravel/PHP package for working with hierarchical tree structures, helping developers model, organize, and traverse nested data with a simple, reusable API for tree-based application features.
This release's headline change is that TreeTrait can now be dropped into your own model to satisfy TreeInterface — previously you were likely locked into extending a package-provided base model. That makes it the first thing to check.
composer require), and run the migrations/publish step if the package ships them.TreeInterface first — it is the contract your application code will type-hint against (TreeInterface $node), so it defines the real public API: parent/children accessors, ancestors/descendants, roots, leaf checks, move/append operations.TreeTrait second — it is the default implementation of that contract and tells you exactly which columns and relations it expects on your table.Category) that already has a parent_id column, add use TreeTrait; and implements TreeInterface, then call $category->children / $category->descendants().Minimal shape:
class Category extends Model implements TreeInterface
{
use TreeTrait;
protected $fillable = ['name', 'parent_id'];
}
Before writing anything custom, confirm whether the package's base model offers extra behaviour (global scopes, ordering, observers) that the trait alone does not — the trait is the portable subset, not necessarily a superset.
Trait on an existing model (the new v1.1 path). Prefer this when you already have a table with domain columns and don't want a package-owned base class in your inheritance chain. Keep your existing extends Model and let the trait supply tree behaviour. This is the pattern to reach for in brownfield apps.
Interface-typed boundaries. Because behaviour is now contract-driven, type-hint TreeInterface in services, actions, and Blade components rather than a concrete class. That lets you have several tree models (categories, pages, org units) share one renderer/traversal service.
Multiple tree models. Define a small abstract base in your app that applies the trait + interface, then extend it per entity — you get the portability of the trait without repeating the boilerplate in five models.
Traversal in services, not views. Wrap descendants(), ancestors(), and roots() in a query/service object so you can swap recursive-CTE implementations later and so controllers stay thin. Return collections and let API resources flatten them.
Rendering a tree. For admin dropdowns, fetch an ordered flat list once and build the indented labels in PHP — this avoids a query per level. For nested JSON, build the structure from a single flat fetch keyed by parent_id rather than lazy-loading children recursively.
Move/append operations. Route all re-parenting through the trait's move/append method (or a wrapper action) instead of assigning parent_id directly, so any path/depth bookkeeping the package performs stays consistent. Wrap moves in a transaction when several nodes shift.
Integration points. Pair with:
use TreeTrait; alone compiles fine but breaks any TreeInterface type-hint or container binding. Always add implements TreeInterface. Conversely, implementing the interface without the trait means writing every method yourself — a silent, large surface area.children(), parent(), or ancestors(). PHP resolves model methods over trait methods, so your version silently wins and the package's expectations break. Audit your existing models for colliding names before applying the trait; use insteadof/aliasing only if you genuinely need both.parent_id and possibly path, depth, order/sort). Read the trait's property references and migration stubs before assuming defaults. Mismatched column names fail at query time, not at boot.$fillable / $guarded. Tree bookkeeping columns are usually written internally; keeping them out of $fillable prevents mass-assignment from silently corrupting paths when you update($request->all()).ORDER BY, descendants come back in whatever order the DB returns. Add an explicit sort column and order scope; don't rely on id ordering.parent_id assignment lets you create A -> B -> A, which will hang recursive traversal or produce infinite loops. Guard in a move action by checking the target is not a descendant of the node.children recursively is the classic performance trap. Fetch a flat, ordered result set and assemble in memory, or push traversal into a recursive CTE where the driver supports it.parent_id nulled), cascade, or block? The trait may rely on a FK constraint you never added.DB::listen or ->dd()); most "wrong tree" bugs are stale path/depth columns from a direct parent_id write, not a broken trait.The overall signal from v1.1.0 is that the package is moving toward contract-based, composable usage rather than inheritance — lean into that by typing against TreeInterface everywhere and treating TreeTrait as a replaceable implementation detail.
How can I help you explore Laravel packages today?