PHP code example of happenv-com / laravel-ltree

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/ */

    

happenv-com / laravel-ltree example snippets


$category->descendants;                              // eager relation, one query
Category::descendantsOf($electronics)->count();      // path <@ :node, GiST-indexed
Category::wherePathMatches('*.Gaming.*')->get();     // lquery
$laptop->moveTo($peripherals);                       // transactional subtree move

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 = [];
}

$electronics = Category::create(['name' => 'Electronics']);            // path: "1"
$computers   = Category::create(['name' => 'Computers', 'parent_id' => $electronics->id]); // path: "1.2"
$laptops     = Category::create(['name' => 'Laptops', 'parent_id' => $computers->id]);     // path: "1.2.3"

$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
    }
}

$laptops->path();              // LtreePath("1.2.3")
(string) $laptops->path();     // "1.2.3"
$laptops->path()->depth();     // 3

$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::query()->whereKey([$a->id, $b->id])->commonAncestor();  // LtreePath|null (lca)
Category::query()->tapPath(fn ($q) => $q->where('active', true)); // tap, keep chaining

Category::roots()->get();
Category::leaves()->get();
Category::descendantsOf($node)->get();
Category::ancestorsOf($node)->get();
Category::childrenOf($node)->get();
Category::siblingsOf($node)->get();

Category::wherePath(fn ($path) => $path
    ->descendantOf($electronics)
    ->depth(3)
    ->contains('Laptops'))
    ->get();

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)

// config/ltree.php  →  'order_column' => 'sort_order'   (default is null = unordered)

$node->moveBefore($sibling);
$node->moveAfter($sibling);
$node->moveFirst();
$node->moveLast();

$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\TreeBuilder;

TreeBuilder::fromArray(Category::class, [
    ['name' => 'Electronics', 'children' => [
        ['name' => 'Computers', 'children' => [
            ['name' => 'Laptops'],
            ['name' => 'Desktops'],
        ]],
        ['name' => 'Phones'],
    ]],
    ['name' => 'Books'],
]);

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' => ['-' => '_'],
    ],
];
bash
php artisan ltree:install
bash
php artisan vendor:publish --tag=ltree-config
bash
php artisan ltree:install                       # publish config + create the ltree extension
php artisan ltree:check "App\Models\Category"   # integrity diagnostic (see below)
php artisan ltree:rebuild "App\Models\Category" # recompute every path from parent_id
php artisan ltree:optimize "App\Models\Category"# ensure the GiST index, then REINDEX + ANALYZE
bash
docker compose up -d
php benchmarks/benchmark.php 50000