Download the PHP package happenv-com/laravel-ltree without Composer

On this page you can find all versions of the php package happenv-com/laravel-ltree. It is possible to download/install these versions without Composer. Possible dependencies are resolved automatically.

FAQ

After the download, you have to make one include require_once('vendor/autoload.php');. After that you have to import the classes with use statements.

Example:
If you use only one package a project is not needed. But if you use more then one package, without a project it is not possible to import the classes with use statements.

In general, it is recommended to use always a project to download your libraries. In an application normally there is more than one library needed.
Some PHP packages are not free to download and because of that hosted in private repositories. In this case some credentials are needed to access such packages. Please use the auth.json textarea to insert credentials, if a package is coming from a private repository. You can look here for more information.

  • Some hosting areas are not accessible by a terminal or SSH. Then it is not possible to use Composer.
  • To use Composer is sometimes complicated. Especially for beginners.
  • Composer needs much resources. Sometimes they are not available on a simple webspace.
  • If you are using private repositories you don't need to share your credentials. You can set up everything on our site and then you provide a simple download link to your team member.
  • Simplify your Composer build process. Use our own command line tool to download the vendor folder as binary. This makes your build process faster and you don't need to expose your credentials for private repositories.
Please rate this library. Is it a good library?

Informations about the package laravel-ltree

Laravel LTree

Laravel LTree

Latest Version on Packagist Total Downloads Tests Mutation PHPStan Zizmor Code Style

PHP 8.3+ Laravel 12 | 13 PostgreSQL 16–18

This package is not another tree implementation. PostgreSQL already provides one. This package exposes it through an expressive Laravel API.

PostgreSQL's ltree extension stores hierarchies as materialized paths and indexes them with GiST. Subtree membership, ancestor lookups, pattern matching and depth queries become single, index-backed operators — no recursive CTEs, no lft/rgt renumbering, no N+1.

laravel-ltree gives you that power through Eloquent: a typed query builder, real (eager-loadable) relations, transactional tree operations, lquery/ltxtquery pattern matching, a tree-aware collection, route-model binding, and Artisan tooling — all fully typed (PHPStan level max), with no raw SQL in your application code.


Table of contents


Requirements

Installation

The service provider is auto-discovered. Then run the installer, which publishes the config file and creates the ltree extension on your default connection:

ltree:install accepts --no-extension (skip CREATE EXTENSION, e.g. when your database user lacks the privilege and a DBA will create it) and --force (overwrite an existing published config). To publish the config manually:

Migrations

The package registers Blueprint macros so your migrations read naturally. A table backed by ltree typically keeps a parent_id adjacency column too — it's optional but recommended (see below):

The Blueprint macros:

Macro Column type
$table->ltree('path') ltree
$table->lquery('col') lquery
$table->ltxtquery('col') ltxtquery
$table->ltreeDepth('depth', from: 'path') integer GENERATED ALWAYS AS (nlevel(path)) STORED
$table->gist('path') GiST index using gist_ltree_ops
$table->gin('path') throws UnsupportedIndexException — there is no GIN opclass for a scalar ltree; use gist()

path is nullable because, with the default primary-key label, a new row's path can only be built once its auto-increment id exists — the package backfills it immediately after insert (see Basic usage). The ltreeDepth() column is a real stored column PostgreSQL keeps in sync, so ordering and filtering by depth never call nlevel() at query time.

Basic usage

Add the HasLtree trait to any Eloquent model:

That's the whole setup. The package maintains path for you from the model's label and its parent. By default the label is the model's primary key, so paths look like 1, 1.5, 1.5.23:

Set the parent by parent_id, or with the fluent helper before saving:

Human-readable labels

Prefer slugs in the path (electronics.computers.laptops)? Override two hooks — return your column as the label, and tell the package the label no longer depends on the primary key:

normalizeLtreeLabel() runs the value through the configured normalizer, which by default replaces characters that are illegal in an ltree label (a hyphen becomes an underscore: gaming-laptops → gaming_laptops). Switch the normalizer to the throw strategy to reject illegal labels with an InvalidLabelException instead.

The path attribute

path is cast to an immutable LtreePath value object, and path() returns it (or null before the row is persisted):

Reading the tree

Every model gets a set of read helpers that answer structural questions without loading relations:

Relationships

All eight relations are real Eloquent relations — they eager-load (with(...)), count, and work with whereHas. The path-based ones issue one query for an entire batch, never one per parent:

Eager loading a forest is a single extra query regardless of how many nodes you load:

whereHas works because the relations compile to correlated ltree predicates:

The query builder

Category::query() returns a typed LtreeBuilder<Category>, so every predicate below is a first-class, IDE-completed method (not a global macro) and PHPStan resolves it at level max. Each predicate accepts a Model, an LtreePath, or a raw path string.

Structural

Depth

Path shape

Aggregates & utilities

Query scopes — the same building blocks as convenient static entry points, each returning an LtreeBuilder:

The wherePath DSL

Compose several path constraints in one grouped WHERE with a fluent, closure-based builder — handy when you want the constraints isolated from surrounding orWhere clauses:

The closure receives a PathConstraint exposing every predicate as a chainable verb: descendantOf, ancestorOf, childOf, parentOf, siblingOf, root, leaf, depth, betweenDepth, maxDepth, minDepth, withinDepth, startsWith, endsWith, segment, matches, matchesAny, contains, containsAll, containsAny.

lquery — path pattern matching

lquery matches a path against a pattern with wildcards (*), quantifiers (*{1,2}), negation (!), and alternation:

lquery is case-sensitive by default; append the @ flag to a label to make just that label case-insensitive: wherePathMatches('*.science@.*').

ltxtquery — full-text label matching

ltxtquery matches whole labels anywhere in the path with boolean operators. The safe helpers validate each word so an operator can't be smuggled in and silently change the query's meaning:

Words match a whole label (so Gam does not match Gaming); append * for a prefix (Gam*) or @ for case-insensitivity (gaming@). For the full raw grammar (&, |, !, grouping) use search():

Tree operations

Every mutating operation runs inside a transaction (toggle with the transactional_moves config), fires cancellable "before" events and "after" events, and uses PostgreSQL's own path arithmetic — a subtree move or rename is a single UPDATE, not a row-by-row walk.

Moving under your own descendant throws an InvalidMoveException.

Ordered siblings (opt-in)

Sibling ordering is opt-in. Add an integer column, point order_column at it (config or per model), and you get ordered moves:

Calling an ordered move without a configured order column throws a MissingSortColumnException.

Collections

Query results come back as an LtreeCollection with tree-shaping helpers — all in memory, plus two that hit the database once for the whole set:

Finders & route-model binding

Bind a route to a model by path with the standard {model:field} syntax:

Non-path fields (and nested resolveChildRouteBinding) fall back to Laravel's default behavior, so {category} still binds by key.

The LtreePath value object

LtreePath is a framework-independent, immutable (final readonly) value object — Countable, IteratorAggregate, Stringable, JsonSerializable. It never touches the database:

Testing helpers

Build trees in tests and seeders without writing nested create() calls. TreeBuilder::fromArray() takes a nested children structure and returns the flat LtreeCollection of everything it created:

Add the HasLtreeFactory trait to a model's factory for a factory-native API:

Artisan commands

ltree:check verifies the extension is installed, the path column has a GiST index, and — reporting a non-zero exit code on any integrity problem — that there are no NULL paths, no orphaned paths (a node whose parent path is missing), no dangling parent_id references, and that path and parent_id agree. It fails cleanly (no stack trace) on an unmigrated table.

Configuration

config/ltree.php:

Any of the per-model column hooks (getLtreePathColumn, getLtreeParentColumn, getLtreeOrderColumn) can be overridden on a model to diverge from the global config.

Indexes

The single index that matters is a GiST index on the path column ($table->gist('path')). It backs every containment (<@, @>), lquery (~), and ltxtquery (@) operator.

There is no GIN operator class for a scalar ltree in PostgreSQL — GIN only applies to ltree[] (arrays of paths). Calling $table->gin('path') therefore throws UnsupportedIndexException with a message pointing you at gist(), rather than silently creating an index that can't serve ltree queries. ltree:optimize creates the GiST index if it's missing.

Performance

The benchmarks/ directory contains a runnable harness comparing this package's ltree approach against a plain parent_id adjacency list queried with recursive CTEs. Indicative numbers from a 50,000-node tree on PostgreSQL 18 (see benchmarks/RESULTS.md for the full, caveated results):

Operation ltree adjacency + recursive CTE
descendants of a node faster (single GiST probe) recursive walk, one join per level
ancestors of a node tied (both bounded by depth) tied
subtree move rewrites the subtree's paths faster (one pointer update)
branch delete faster (DELETE … WHERE path <@ ?) recursive delete

The honest tradeoff: adjacency wins the move because it only re-points one parent_id — but it pays that back on every descendant/ancestor read, forever, via a recursive CTE. ltree pays the rewrite once, at move time, and keeps every read a single index probe. Read-heavy hierarchies (permissions, categories, comment threads, org charts) that are rarely restructured favor ltree; write-heavy, move-heavy trees with rare membership queries favor adjacency.

Best practices

Why not parent_id?

A plain parent_id adjacency list is the natural first model, and this package keeps it around. But parent_id alone can't answer the questions hierarchies are actually about — "everything under this node", "the whole ancestor chain", "everything three levels deep" — without a recursive CTE that re-walks the tree on every read, one join per level, for the lifetime of the data. There's no single index that makes those reads fast.

ltree stores the answer — the materialized path — in an indexed column. "Everything under this node" becomes path <@ :node, a single GiST index probe, whatever the depth. You keep parent_id for what it's genuinely good at (a simple, FK-enforceable pointer to the immediate parent) and let ltree serve the subtree/ancestor/pattern queries.

Why not nested sets?

Nested sets (lft/rgt) also make subtree reads fast, but they encode position as a pair of boundary numbers spanning the entire tree. Inserting or moving a node has to renumber every node to its right — a write touching a large fraction of the table, wrapped in a lock to stay consistent. That's painful for anything write-heavy, and the invariants are easy to corrupt.

ltree is a materialized path, not a global numbering. A move rewrites only the moved subtree's paths (O(subtree), not O(tree)); inserts touch one row; and there is no fragile global invariant to keep in sync — a path is self-describing. You get nested-set-class read performance without nested-set write costs, and PostgreSQL maintains the index for you.

Events

Each tree operation dispatches a cancellable "before" event (return false from a listener to abort) and an "after" event:

Operation Before After
moveTo / detach Moving Moved
copyTo Copying Copied
cascadeDelete CascadeDeleting CascadeDeleted
renameSegment Renaming Renamed
rebuildPaths Rebuilding Rebuilt

All live in Happenv\Ltree\Events.

FAQ

Do my models have to implement an interface? No. Add the HasLtree trait and you're done — it's a zero-config, interface-free trait.

Can the path use names/slugs instead of ids? Yes — override getLtreeLabel() and return false from getLtreeLabelDependsOnKey(). See Human-readable labels.

Does it work with UUID primary keys? Yes. With a key-based label the path is composed of the keys; with HasUuids the path is built from the UUIDs.

Do I have to write any SQL? No. Every operator and function is exposed through typed methods with bound parameters. If you ever want the raw function expressions (nlevel, subpath, lca, …) they're available via Happenv\Ltree\Support\Ltree.

Is it safe against SQL injection? Yes. Every user-supplied value is bound; identifiers come from your config and are quoted.

Which PostgreSQL versions are supported? 16, 17, and 18 — the full suite is run against all three in CI.

Quality

License

The MIT License (MIT). See LICENSE.


All versions of laravel-ltree with dependencies

PHP Build Version
Package Version
Requires php Version ^8.3
illuminate/database Version ^12.0|^13.0
illuminate/support Version ^12.0|^13.0
Composer command for our command line client (download client) This client runs in each environment. You don't need a specific PHP version etc. The first 20 API calls are free. Standard composer command

The package happenv-com/laravel-ltree contains the following files

Loading the files please wait ...