1. Go to this page and download the library: Download happenv-com/laravel-ltree library. Choose the download type require.
2. Extract the ZIP file and open the index.php.
3. Add this code to the index.php.
<?php
require_once('vendor/autoload.php');
/* Start to develop here. Best regards https://php-download.com/ */
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
public function up(): void
{
Schema::create('categories', function (Blueprint $table) {
$table->id();
$table->string('name');
$table->foreignId('parent_id')->nullable()->constrained('categories')->nullOnDelete();
$table->ltree('path')->nullable(); // the materialized path
$table->ltreeDepth(); // STORED generated column: nlevel(path)
$table->gist('path'); // GiST index — the index that makes ltree fast
});
}
};
use Happenv\Ltree\Concerns\HasLtree;
use Illuminate\Database\Eloquent\Model;
class Category extends Model
{
use HasLtree;
protected $guarded = [];
}
$laptops = new Category(['name' => 'Laptops']);
$laptops->setLtreeParent($computers)->save(); // path derived from $computers
class Category extends Model
{
use HasLtree;
public function getLtreeLabel(): string
{
return $this->normalizeLtreeLabel($this->slug); // e.g. "gaming-laptops"
}
public function getLtreeLabelDependsOnKey(): bool
{
return false; // label comes from the slug, not the auto-increment id
}
}
$node->depth(); // nlevel(path) — 1 for a root
$node->level(); // alias of depth()
$node->isRoot(); // depth === 1
$node->isLeaf(); // has no descendants
$node->hasParent();
$node->hasChildren();
$node->hasDescendants();
$node->countChildren(); // direct children
$node->countDescendants(); // whole subtree, excluding self
$node->branch(); // query builder for this node's whole subtree (self + descendants)
$node->parent; // BelongsTo (via parent_id)
$node->children; // HasMany (via parent_id)
$node->descendants; // strict descendants (path <@ node)
$node->descendantsAndSelf; // descendants including the node
$node->ancestors; // strict ancestors (path @> node), root-first
$node->ancestorsAndSelf; // ancestors including the node
$node->siblings; // same parent, excluding self
$node->root; // the top-most (depth-1) ancestor; a root's root is itself
$roots = Category::whereNull('parent_id')->with('descendants')->get(); // 2 queries total
Category::whereHas('descendants', fn ($q) => $q->where('name', 'Laptops'))->get();
Category::whereHas('children')->get(); // only nodes that actually have children
Category::query()->whereRoot();
Category::query()->whereLeaf();
Category::query()->whereAncestorOf($node);
Category::query()->whereDescendantOf($node);
Category::query()->whereChildOf($node);
Category::query()->whereParentOf($node);
Category::query()->whereSiblingOf($node);
Category::query()->whereDescendantOfAny([$a, $b, $c]); // union across several nodes, one query
Category::query()->whereAncestorOfAny([$a, $b]);
Category::query()->whereDepth(3);
Category::query()->whereBetweenDepth(2, 4);
Category::query()->maxDepth(3);
Category::query()->minDepth(2);
Category::query()->whereWithinDepthOf($node, 2); // node + up to 2 levels below
Category::query()->orderByDepth('desc');
Category::query()->withDepth(); // adds a "depth" column
Category::query()->wherePathStartsWith($ancestor); // :ancestor @> path
Category::query()->wherePathEndsWith('Laptops'); // lquery *.Laptops
Category::query()->whereSegment('Gaming'); // label anywhere in the path
Category::wherePathMatches('*.Gaming.*')->get(); // Gaming anywhere in the path
Category::wherePathMatches('Top.*{1}.Laptops')->get(); // exactly one label between Top and Laptops
Category::wherePathMatches('*.!Physics')->get(); // last label is not "Physics"
Category::wherePathMatches('*.Science.*')
->orWherePathMatches('*.Hobbies.*')
->get();
Category::wherePathMatchesAny(['*.Astronomy', '*.Physics'])->get(); // match ANY of several lqueries
Category::wherePathContains('Gaming')->get(); // has a "Gaming" label
Category::wherePathContainsAll(['Gaming', 'Laptop'])->get(); // Gaming AND Laptop
Category::wherePathContainsAny(['Laptop', 'Desktop'])->get(); // Laptop OR Desktop
Category::search('Gaming & !Console')->get();
$laptops->moveTo($peripherals); // reparent the whole subtree; guards against moving under itself
$laptops->detach(); // make it a new root (moveTo(null))
$copy = $laptops->copyTo($store); // deep-copy the subtree with new keys; returns the new root
$node->appendChild($child); // attach as a child
$node->prependChild($child);
$branch->cascadeDelete(); // DELETE the whole subtree; alias: deleteBranch()
$node->renameSegment('notebooks');// rename this node's label; cascades to every descendant path
Category::query()->first()->rebuildPaths(); // recompute every path from parent_id (repair / migration)
$flat = Category::query()->whereDescendantOf($root)->get();
$tree = $flat->toTree(); // nest into a tree; each node's `children` relation is populated
$tree->flatten(); // inverse of toTree()
$flat->roots(); // members with no parent in the set
$flat->leaves(); // members that aren't a parent of another member
$flat->sortTree(); // depth-first (path) order
$flat->descendants(); // ONE query: every descendant of every member, minus the members
$flat->ancestors(); // ONE query: every ancestor of every member
Category::findByPath('1.2.3'); // ?Category by path string
Category::findByLtree($ltreePath); // ?Category by LtreePath
Category::findDescendantsOf($node);// LtreeCollection
Category::findAncestorsOf($node); // LtreeCollection
Route::get('/categories/{category:path}', fn (Category $category) => $category);
// GET /categories/1.2.3 → resolves the category whose path is "1.2.3"
use Happenv\Ltree\ValueObjects\LtreePath;
$path = new LtreePath('electronics.computers.laptops');
$path->segments(); // ['electronics', 'computers', 'laptops']
$path->depth(); // 3
$path->parent(); // LtreePath("electronics.computers") | null at the root
$path->root(); // LtreePath("electronics")
$path->first(); $path->last(); // "electronics" / "laptops"
$path->append('gaming'); // LtreePath("...laptops.gaming") (returns a new instance)
$path->prepend('catalog');
$path->slice(1, 2);
$path->contains('computers'); // true
$path->startsWith('electronics'); // true
$path->endsWith('laptops'); // true
$path->isAncestorOf($other);
$path->isDescendantOf($other);
$path->equals($other);
$path->compareLexically($other); // -1 | 0 | 1, for sorting
(string) $path; // "electronics.computers.laptops"
use Happenv\Ltree\Testing\HasLtreeFactory;
class CategoryFactory extends Factory
{
use HasLtreeFactory;
// ...
}
// Build a standalone tree from the factory:
Category::factory()->createTree([...]);
// Or attach a subtree beneath each created parent — mirrors Laravel's has():
Category::factory()->hasTree([
['children' => [[], []]],
])->create();
return [
'path_column' => 'path',
'parent_column' => 'parent_id', // set null to run purely on ltree, no adjacency mirror
'order_column' => null, // set to e.g. 'sort_order' to enable ordered siblings
'auto_update_path' => true, // maintain `path` automatically via the model observer
'auto_create_extension'=> false, // CREATE EXTENSION during migrations
'default_index' => 'gist',
'transactional_moves' => true, // wrap tree operations in a transaction
'cascade_delete' => false,
'fk_on_delete' => 'restrict',
'normalizer' => [
'class' => \Happenv\Ltree\Normalization\DefaultNormalizer::class,
'strategy' => 'replace', // 'replace' illegal chars, or 'throw'
'replacements' => ['-' => '_'],
],
];