PHPackages                             tetthys/eloquent-hierarchy - PHPackages - PHPackages  [Skip to content](#main-content)[PHPackages](/)[Directory](/)[Categories](/categories)[Trending](/trending)[Leaderboard](/leaderboard)[Changelog](/changelog)[Analyze](/analyze)[Collections](/collections)[Log in](/login)[Sign up](/register)

1. [Directory](/)
2. /
3. [Database &amp; ORM](/categories/database)
4. /
5. tetthys/eloquent-hierarchy

ActiveLibrary[Database &amp; ORM](/categories/database)

tetthys/eloquent-hierarchy
==========================

Lightweight Eloquent trait for self-referential (recursive) models with parent/children relations and O(1)/EXISTS() checks.

0.0.1(6mo ago)135MITPHPPHP ^8.3

Since Oct 20Pushed 6mo agoCompare

[ Source](https://github.com/tetthys/eloquent-hierarchy)[ Packagist](https://packagist.org/packages/tetthys/eloquent-hierarchy)[ RSS](/packages/tetthys-eloquent-hierarchy/feed)WikiDiscussions dev Synced 1mo ago

READMEChangelog (1)Dependencies (5)Versions (2)Used By (0)

Eloquent Hierarchy (Adjacency List)
===================================

[](#eloquent-hierarchy-adjacency-list)

A lightweight, high-performance Eloquent trait for self-referential (recursive) models.
It provides parent/children relations, O(1) `hasParent()`, `EXISTS`-based `hasChildren()`, depth calculation, ancestor/descendant utilities (CTE fast path + BFS fallback), and handy query scopes.

---

Features
--------

[](#features)

- **Relations**: `parent()` and `children()` (self-referencing)
- **Fast checks**: `hasParent()` (O(1), no query), `hasChildren()` (single `EXISTS`)
- **Depth**: `depth()` with eager-load shortcut, cycle/missing-parent guards
- **Ancestors/Descendants**:
    - **Fast path**: single-query **recursive CTE** (MySQL 8+, PostgreSQL, SQLite)
    - **Fallback**: **batched BFS** traversal for engines without recursive CTE
    - Stream descendants as **`LazyCollection`**
- **Scopes**: `roots()` (no parent), `leaves()` (no children)
- **Customizable**: override FK/PK naming via constant or methods
- **External SQL templates**: editable CTE SQL in `src/Sql/*.sql`

---

Requirements
------------

[](#requirements)

- PHP **8.0+** (tested up to PHP 8.3+)
- Laravel / Illuminate Database **v10+** (works with 10 / 11 / 12)
- Database:
    - **CTE fast path**: MySQL **8+**, PostgreSQL, SQLite
    - **BFS fallback**: works anywhere Eloquent runs (e.g., SQL Server)

> The package ships SQL templates for CTE queries in `src/Sql/descendant_ids_cte.sql` and `src/Sql/descendants_exist_cte.sql`.

---

Installation
------------

[](#installation)

```
composer require tetthys/eloquent-hierarchy

```

If you are developing this repo locally (using the included Docker setup) and see Composer plugin prompts (e.g., for Pest), allow it explicitly:

```
composer config --no-plugins allow-plugins.pestphp/pest-plugin true

```

---

File Layout (key parts)
-----------------------

[](#file-layout-key-parts)

```
src/
  Concerns/
    HasHierarchy.php            # The trait
  Sql/
    descendant_ids_cte.sql      # CTE for collecting descendant IDs
    descendants_exist_cte.sql   # CTE for existence probe (LIMIT 1)

```

`HasHierarchy` loads the SQL templates at runtime and replaces placeholders like `{{table}}`, `{{pk}}`, `{{parent_fk}}`, and `{{depth_limit}}`.

---

Quick Start
-----------

[](#quick-start)

### 1) Add the trait to your model

[](#1-add-the-trait-to-your-model)

```
use Illuminate\Database\Eloquent\Model;
use Tetthys\EloquentHierarchy\Concerns\HasHierarchy;

class Category extends Model
{
    use HasHierarchy;

    // Optional: override the parent FK via a constant
    // public const HIERARCHY_PARENT_FOREIGN_KEY = 'parent_id';

    protected $fillable = ['name', 'parent_id'];
}

```

### 2) Migration (example)

[](#2-migration-example)

```
Schema::create('categories', function (Blueprint $table) {
    $table->id();
    $table->string('name');
    $table->unsignedBigInteger('parent_id')->nullable()->index();
    $table->timestamps();
});

```

### 3) Basic usage

[](#3-basic-usage)

```
$root = Category::create(['name' => 'Root']);
$child = Category::create(['name' => 'Child', 'parent_id' => $root->id]);

$child->hasParent();   // true (O(1), no query)
$root->hasChildren();  // true (EXISTS query)

$child->parent;        // BelongsTo relation (Category)
$root->children;       // HasMany relation (Collection)
$child->depth();       // 1

```

---

Ancestors &amp; Depth
---------------------

[](#ancestors--depth)

```
$leaf = Category::create(['name' => 'Leaf', 'parent_id' => $child->id]);

$leaf->depth();                // 2
$leaf->ancestors()->all();     // [Child, Root]
$leaf->ancestorIds()->all();   // [child_id, root_id]

$root->isAncestorOf($leaf);    // true
$leaf->isDescendantOf($root);  // true

// Limit how far to walk upward
$leaf->ancestors(1)->all();    // [Child]

```

> **Tip**: If you eager load `parent.parent...`, `depth()` and `ancestors()` can walk **in memory** with 0 queries.

---

Descendants (CTE fast path + BFS fallback)
------------------------------------------

[](#descendants-cte-fast-path--bfs-fallback)

These APIs automatically use a **recursive CTE** when your driver supports it; otherwise they switch to a **BFS** strategy optimized to minimize data transfer.

```
$root->descendantsExist();        // true if any descendant exists
$root->descendantsExist(1);       // true if any direct child exists
$root->descendantsExist(2);       // true if child or grandchild exists

$ids = $root->descendantIds()->all();     // [child_id, grandchild_id, ...]
$ids = $root->descendantIds(1)->all();    // only depth=1

// Stream IDs level-by-level (no big arrays in memory)
foreach ($root->descendantIds() as $id) {
    // process each descendant id
}

// Materialize models (keeps level-order)
$models = $root->descendants(['id', 'name']);  // Collection

```

---

Query Scopes
------------

[](#query-scopes)

```
Category::roots()->get();   // parent_id IS NULL
Category::leaves()->get();  // no children

```

---

Customization
-------------

[](#customization)

### Change the parent foreign key column (no method override needed)

[](#change-the-parent-foreign-key-column-no-method-override-needed)

```
class Category extends Model
{
    use HasHierarchy;

    public const HIERARCHY_PARENT_FOREIGN_KEY = 'parent_uuid';
}

```

### Or override the methods directly

[](#or-override-the-methods-directly)

```
class Category extends Model
{
    use HasHierarchy;

    protected function parentForeignKeyName(): string
    {
        return 'parent_uuid';
    }

    protected function ownerKeyName(): string
    {
        return 'uuid';
    }
}

```

---

How it works (short version)
----------------------------

[](#how-it-works-short-version)

- **Depth &amp; Ancestors**: walk up using eager-loaded parents when available; otherwise perform tiny per-hop lookups (`SELECT pk, parent_fk`).
- **Descendants**:

    - **CTE**: `WITH RECURSIVE` to expand the subtree in one query. SQL lives in `src/Sql/*.sql` and is loaded + templated at runtime.
    - **BFS**: level-order scan using `whereIn(parent_fk, frontier)` and `pluck(pk)` in batches to reduce memory and round-trips.

All traversals include **cycle** and **missing-parent** guards.

---

Performance Notes
-----------------

[](#performance-notes)

- Prefer **CTE-capable DBs** (MySQL 8+, PostgreSQL, SQLite) to enable single-query descendant expansion.
- When falling back to BFS, tune the `chunkSize` argument of `descendantIds($maxDepth, $chunkSize)`.
- Eager load `parent.parent...` for top-down depth/ancestor operations to avoid extra queries.

---

Testing locally (optional)
--------------------------

[](#testing-locally-optional)

This repo includes a minimal Docker + Pest setup for local testing.

```
# build + install + run tests
bash ./run/test.sh

# pass flags through to Pest
bash ./run/test.sh -- --filter=Descendants

```

> If Composer blocks a dev plugin (like Pest), you can allow it:
>
> ```
> composer config --no-plugins allow-plugins.pestphp/pest-plugin true
>
> ```

---

FAQ
---

[](#faq)

**Q: Does this require a full Laravel app?**A: No. It only needs Eloquent. Tests use Orchestra Testbench.

**Q: Can I customize the SQL?**A: Yes. Edit the files in `src/Sql/`. The trait replaces placeholders and binds parameters positionally.

**Q: Do I need to call anything to choose CTE or BFS?**A: No. The trait auto-detects driver support and picks the best path.

---

License
-------

[](#license)

MIT

###  Health Score

32

—

LowBetter than 72% of packages

Maintenance66

Regular maintenance activity

Popularity9

Limited adoption so far

Community2

Small or concentrated contributor base

Maturity40

Maturing project, gaining track record

How is this calculated?**Maintenance (25%)** — Last commit recency, latest release date, and issue-to-star ratio. Uses a 2-year decay window.

**Popularity (30%)** — Total and monthly downloads, GitHub stars, and forks. Logarithmic scaling prevents top-heavy scores.

**Community (15%)** — Contributors, dependents, forks, watchers, and maintainers. Measures real ecosystem engagement.

**Maturity (30%)** — Project age, version count, PHP version support, and release stability.

###  Release Activity

Cadence

Unknown

Total

1

Last Release

205d ago

### Community

Maintainers

![](https://www.gravatar.com/avatar/858f92afec0ff81e6888c9ae6f363b56ebf82e45a32d9e1b37341569c5d1b267?d=identicon)[tetthys](/maintainers/tetthys)

###  Code Quality

TestsPest

### Embed Badge

![Health badge](/badges/tetthys-eloquent-hierarchy/health.svg)

```
[![Health](https://phpackages.com/badges/tetthys-eloquent-hierarchy/health.svg)](https://phpackages.com/packages/tetthys-eloquent-hierarchy)
```

###  Alternatives

[owen-it/laravel-auditing

Audit changes of your Eloquent models in Laravel

3.4k33.0M95](/packages/owen-it-laravel-auditing)[staudenmeir/eloquent-json-relations

Laravel Eloquent relationships with JSON keys

1.1k5.8M24](/packages/staudenmeir-eloquent-json-relations)[bavix/laravel-wallet

It's easy to work with a virtual wallet.

1.3k1.1M11](/packages/bavix-laravel-wallet)[dragon-code/migrate-db

Easy data transfer from one database to another

15717.4k](/packages/dragon-code-migrate-db)[gearbox-solutions/eloquent-filemaker

A package for getting FileMaker records as Eloquent models in Laravel

6454.8k2](/packages/gearbox-solutions-eloquent-filemaker)[cybercog/laravel-ownership

Laravel Ownership simplify management of Eloquent model's owner.

9126.6k3](/packages/cybercog-laravel-ownership)

PHPackages © 2026

[Directory](/)[Categories](/categories)[Trending](/trending)[Changelog](/changelog)[Analyze](/analyze)
